From 76d315724773dee550ba5a5035ad3823cd8ab127 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Tue, 25 Apr 2017 10:26:48 +0200 Subject: [PATCH 001/288] travis push to dockerhub --- .travis.yml | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 51c40faf..e28f2031 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,19 @@ before_install: - 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 haveged && sudo service haveged start +after_success: + - docker login -e $DOCKER_EMAIL -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 + - TESSDATA_PREFIX=/usr/share/tesseract-ocr + - LC_NUMERIC=C + - 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} From 60021e512341e830ffa6ca378ab61d34f3730216 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Tue, 25 Apr 2017 10:39:58 +0200 Subject: [PATCH 002/288] build prod package before pushing to dockerhub --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index e28f2031..6415cfd4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,8 @@ before_install: - sudo apt-get -y -q install tesseract-ocr tesseract-ocr-fra tesseract-ocr-jpn - sudo apt-get -y -q install haveged && sudo service haveged start after_success: - - docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS + - 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 . From 6b0106e3850a09343f4900b11bbc5ce9b7dafc82 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Tue, 25 Apr 2017 11:09:18 +0200 Subject: [PATCH 003/288] update readme with docker instructions --- README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d2f9eb4f..0228e56a 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,16 @@ Download -------- 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/) +You will need a Java webapp server to run it, like [Jetty](http://eclipse.org/jetty/) or [Tomcat](http://tomcat.apache.org/). +The default admin password is "admin". Don't forget to change it before going to production. + +Install with Docker +------------------- + +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 -p 8100:8080 -v sismics_docs_latest:/data sismics/docs:latest How to build Docs from the sources ---------------------------------- From c352b94b38e55a3e50f68b165b2a02eec800a53f Mon Sep 17 00:00:00 2001 From: jendib Date: Sun, 7 May 2017 01:25:20 +0200 Subject: [PATCH 004/288] Closes #126: click to copy --- .../webapp/src/app/docs/controller/document/DocumentView.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentView.js b/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentView.js index 1c1ce194..c8615295 100644 --- a/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentView.js +++ b/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentView.js @@ -100,7 +100,8 @@ angular.module('docs').controller('DocumentView', function ($scope, $state, $sta var title = 'Shared document'; var msg = 'You can share this document by giving this link. ' + 'Note that everyone having this link can see the document.
' + - ''; + ''; var btns = [ {result: 'unshare', label: 'Unshare', cssClass: 'btn-danger'}, {result: 'close', label: 'Close'} From e38bdbe50801106e34961ec2e26bf51a460a5458 Mon Sep 17 00:00:00 2001 From: jendib Date: Sun, 7 May 2017 01:27:24 +0200 Subject: [PATCH 005/288] Closes #128: Delete cursor on comment delete button --- docs-web/src/main/webapp/src/partial/docs/document.view.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs-web/src/main/webapp/src/partial/docs/document.view.html b/docs-web/src/main/webapp/src/partial/docs/document.view.html index 3e94f557..8525d2eb 100644 --- a/docs-web/src/main/webapp/src/partial/docs/document.view.html +++ b/docs-web/src/main/webapp/src/partial/docs/document.view.html @@ -107,7 +107,7 @@

{{ comment.content }}
{{ comment.create_date | date: 'yyyy-MM-dd' }} - Delete

From 5f7d2f2a685bbc876b059376e1de036486fdf206 Mon Sep 17 00:00:00 2001 From: jendib Date: Sun, 7 May 2017 01:31:59 +0200 Subject: [PATCH 006/288] Closes #129: bigger checkbox --- .../src/main/webapp/src/partial/docs/document.default.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs-web/src/main/webapp/src/partial/docs/document.default.html b/docs-web/src/main/webapp/src/partial/docs/document.default.html index c15b146b..dd0a6aca 100644 --- a/docs-web/src/main/webapp/src/partial/docs/document.default.html +++ b/docs-web/src/main/webapp/src/partial/docs/document.default.html @@ -10,9 +10,9 @@ -
+
- +
From cbfa4b1c417c8932cf707feab35facbf47a3acb8 Mon Sep 17 00:00:00 2001 From: jendib Date: Sun, 7 May 2017 01:32:55 +0200 Subject: [PATCH 007/288] Closes #127: Edit -> Save --- docs-web/src/main/webapp/src/partial/docs/document.edit.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs-web/src/main/webapp/src/partial/docs/document.edit.html b/docs-web/src/main/webapp/src/partial/docs/document.edit.html index 68fca1e4..30e9fc9b 100644 --- a/docs-web/src/main/webapp/src/partial/docs/document.edit.html +++ b/docs-web/src/main/webapp/src/partial/docs/document.edit.html @@ -129,7 +129,7 @@
- +
From 3274b4c79ae2b520cc91bc0c2466d2359e8762a4 Mon Sep 17 00:00:00 2001 From: jendib Date: Sun, 7 May 2017 01:34:21 +0200 Subject: [PATCH 008/288] Closes #130: Fix document language icon --- docs-web/src/main/webapp/src/partial/docs/document.view.html | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs-web/src/main/webapp/src/partial/docs/document.view.html b/docs-web/src/main/webapp/src/partial/docs/document.view.html index 8525d2eb..017c0b1e 100644 --- a/docs-web/src/main/webapp/src/partial/docs/document.view.html +++ b/docs-web/src/main/webapp/src/partial/docs/document.view.html @@ -40,9 +40,10 @@ diff --git a/docs-web/src/main/webapp/src/partial/docs/document.view.html b/docs-web/src/main/webapp/src/partial/docs/document.view.html index 017c0b1e..f6f27bb2 100644 --- a/docs-web/src/main/webapp/src/partial/docs/document.view.html +++ b/docs-web/src/main/webapp/src/partial/docs/document.view.html @@ -41,7 +41,6 @@ From 198a6d566501a6b5fd8d9d3e2bd19fdce04017ca Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Wed, 1 Nov 2017 19:48:50 +0100 Subject: [PATCH 023/288] #111: translate templates (wip) --- .../main/webapp/src/app/docs/directive/Acl.js | 2 +- .../webapp/src/app/docs/filter/Filesize.js | 6 +- docs-web/src/main/webapp/src/locale/en.json | 189 ++++++++++++++++-- .../src/partial/docs/directive.acledit.html | 18 +- .../src/partial/docs/directive.auditlog.html | 12 +- .../docs/directive.selectrelation.html | 2 +- .../src/partial/docs/directive.selecttag.html | 2 +- .../src/partial/docs/document.default.html | 20 +- .../src/partial/docs/document.edit.html | 59 +++--- .../webapp/src/partial/docs/document.html | 35 ++-- .../webapp/src/partial/docs/document.pdf.html | 16 +- .../src/partial/docs/document.share.html | 8 +- .../partial/docs/document.view.activity.html | 2 +- .../partial/docs/document.view.content.html | 22 +- .../src/partial/docs/document.view.html | 38 ++-- .../docs/document.view.permissions.html | 18 +- .../webapp/src/partial/docs/file.view.html | 6 +- .../src/partial/docs/group.profile.html | 8 +- .../main/webapp/src/partial/docs/main.html | 2 +- .../src/partial/docs/settings.account.html | 18 +- .../src/partial/docs/settings.config.html | 30 ++- .../src/partial/docs/settings.default.html | 1 - docs-web/src/main/webapp/src/style/main.less | 13 ++ 23 files changed, 351 insertions(+), 176 deletions(-) diff --git a/docs-web/src/main/webapp/src/app/docs/directive/Acl.js b/docs-web/src/main/webapp/src/app/docs/directive/Acl.js index ca1efbcc..3fd0a22c 100644 --- a/docs-web/src/main/webapp/src/app/docs/directive/Acl.js +++ b/docs-web/src/main/webapp/src/app/docs/directive/Acl.js @@ -6,7 +6,7 @@ angular.module('docs').directive('acl', function() { return { restrict: 'E', - template: '{{ data.type == \'SHARE\' ? \'Shared\' : (data.type == \'USER\' ? \'User\' : \'Group\') }} {{ data.name }}', + template: '{{ \'acl.\' + data.type | translate }} {{ data.name }}', replace: true, scope: { data: '=' diff --git a/docs-web/src/main/webapp/src/app/docs/filter/Filesize.js b/docs-web/src/main/webapp/src/app/docs/filter/Filesize.js index e3dfda68..26e01540 100644 --- a/docs-web/src/main/webapp/src/app/docs/filter/Filesize.js +++ b/docs-web/src/main/webapp/src/app/docs/filter/Filesize.js @@ -3,7 +3,7 @@ /** * Format file sizes. */ -angular.module('docs').filter('filesize', function() { +angular.module('docs').filter('filesize', function($translate) { return function(text) { if (!text) { return ''; @@ -11,8 +11,8 @@ angular.module('docs').filter('filesize', function() { var size = parseInt(text); if (size > 1000000) { // 1MB - return Math.round(size / 1000000) + 'MB'; + return Math.round(size / 1000000) + $translate.instant('filter.filesize.mb'); } - return Math.round(size / 1000) + 'kB'; + return Math.round(size / 1000) + $translate.instant('filter.filesize.kb'); } }); \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/locale/en.json b/docs-web/src/main/webapp/src/locale/en.json index ae1be947..17cc5573 100644 --- a/docs-web/src/main/webapp/src/locale/en.json +++ b/docs-web/src/main/webapp/src/locale/en.json @@ -22,32 +22,124 @@ "logout": "Logout" }, "document": { + "add_document": "Add a document", + "tags": "Tags", + "no_tags": "No tags", + "no_documents": "No document in the database", + "search": "Search", + "search_empty": "No matches for \"{{ search }}\"", + "shared": "Shared", + "title": "Title", + "description": "Description", + "contributors": "Contributors", + "language": "Language", + "creation_date": "Creation date", + "subject": "Subject", + "identifier": "Identifier", + "publisher": "Publisher", + "format": "Format", + "source": "Source", + "type": "Type", + "coverage": "Coverage", + "rights": "Rights", + "relations": "Relations", + "page_size": "Page size", + "page_size_10": "10 per page", + "page_size_20": "20 per page", + "page_size_30": "20 per page", + "upgrade_quota": "To upgrade your quota, ask your administrator", + "quota": "{{ current | number: 0 }}MB ({{ percent | number: 1 }}%) used on {{ total | number: 0 }}MB", + "count": "{{ count }} document{{ count > 1 ? 's' : '' }} found", "view": { - "content": { - "delete_file_title": "Delete file", - "delete_file_message": "Do you really want to delete this file?", - "upload_pending": "Pending...", - "upload_progress": "Uploading...", - "upload_error": "Upload error", - "upload_error_quota": "Quota reached" - }, "delete_comment_title": "Delete comment", "delete_comment_message": "Do you really want to delete this comment?", "delete_document_title": "Delete document", "delete_document_message": "Do you really want to delete this document?", "shared_document_title": "Shared document", - "shared_document_message": "You can share this document by giving this link. Note that everyone having this link can see the document.
" + "shared_document_message": "You can share this document by giving this link. Note that everyone having this link can see the document.
", + "not_found": "Document not found", + "forbidden": "Access forbidden", + "download_files": "Download files", + "export_pdf": "Export to PDF", + "by_creator": "by", + "comments": "Comments", + "no_comments": "No comments on this document yet", + "add_comment": "Add a comment", + "error_loading_comments": "Error loading comments", + "content": { + "content": "Content", + "delete_file_title": "Delete file", + "delete_file_message": "Do you really want to delete this file?", + "upload_pending": "Pending...", + "upload_progress": "Uploading...", + "upload_error": "Upload error", + "upload_error_quota": "Quota reached", + "drop_zone": "Drag & drop files here to upload" + }, + "permissions": { + "permissions": "Permissions", + "message": "Permissions can be applied directly to this document, or can come from tags.", + "title": "Permissions on this document", + "inherited_tags": "Permissions inherited by tags", + "acl_source": "From", + "acl_target": "For", + "acl_permission": "Permission" + }, + "activity": { + "activity": "Activity", + "message": "Every actions on this document are logged here." + } }, "edit": { "document_edited_with_errors": "Document successfully edited but some files cannot be uploaded", "document_added_with_errors": "Document successfully added but some files cannot be uploaded", - "quota_reached": "Quota reached" + "quota_reached": "Quota reached", + "primary_metadata": "Primary metadata", + "title_placeholder": "The nature or genre of the resource", + "description_placeholder": "An account of the resource", + "new_files": "New files", + "orphan_files": "+ {{ count }} file{{ count > 1 ? 's' : '' }}", + "additional_metadata": "Additional metadata", + "subject_placeholder": "The topic of the resource", + "identifier_placeholder": "An unambiguous reference to the resource within a given context", + "publisher_placeholder": "An entity responsible for making the resource available", + "format_placeholder": "The file format, physical medium, or dimensions of the resource", + "source_placeholder": "A related resource from which the described resource is derived", + "uploading_files": "Uploading files..." }, "default": { "upload_pending": "Pending...", "upload_progress": "Uploading...", "upload_error": "Upload error", - "upload_error_quota": "Quota reached" + "upload_error_quota": "Quota reached", + "quick_upload": "Quick upload", + "drop_zone": "Drag & drop files here to upload", + "add_new_document": "Add to new document", + "latest_activity": "Latest activity", + "footer_sismics": "Crafted with by Sismics", + "api_documentation": "API Documentation", + "version": "Version:", + "memory": "Memory:" + }, + "pdf": { + "export_title": "Export to PDF", + "export_metadata": "Export metadata", + "export_comments": "Export comments", + "fit_to_page": "Fit image to page", + "margin": "Margin", + "millimeter": "mm" + }, + "share": { + "title": "Share document", + "message": "Name the sharing if you want to share multiple times the same document.", + "submit": "Share" + } + }, + "file": { + "view": { + "previous": "Previous", + "next": "Next", + "not_found": "File not found" } }, "tag": { @@ -56,6 +148,14 @@ "delete_tag_message": "Do you really want to delete this tag?" } }, + "group": { + "rofile": { + "members": "Members", + "no_members": "No member", + "related_links": "Related links", + "edit_group": "Edit {{ name }} group" + } + }, "settings": { "user": { "edit": { @@ -74,11 +174,76 @@ } }, "account": { + "password": "Password", + "password_confirm": "Password (confirm)", "updated": "Account successfully updated" + }, + "config": { + "title_guest_access": "Guest access", + "message_guest_access": "Guest access is a mode where anyone can access {{ appName }} without password.
Like a normal user, the guest user can only access its documents and those accessible through permissions.
", + "enable_guest_access": "Enable guest access", + "disable_guest_access": "Disable guest access", + "title_theme": "Theme customization", + "application_name": "Application name", + "main_color": "Main color", + "custom_css": "Custom CSS", + "custom_css_placeholder": "Custom CSS to add after the main stylesheet", + "logo": "Logo (squared size)", + "background_image": "Background image", + "uploading_image": "Uploading the image..." } }, + "directive": { + "acledit": { + "acl_target": "For", + "acl_permission": "Permission", + "add_permission": "Add a permission", + "search_user_group": "Search a user or group" + }, + "auditlog": { + "log_created": "created", + "log_updated": "updated", + "log_deleted": "deleted" + }, + "selectrelation": { + "typeahead": "Type a document title" + }, + "selecttag": { + "typeahead": "Type a tag" + } + }, + "filter": { + "filesize": { + "mb": "MB", + "kb": "kB" + } + }, + "acl": { + "READ": "Can read", + "READWRITE": "Can write", + "WRITE": "Can write", + "USER": "User", + "GROUP": "Group", + "SHARE": "Shared" + }, + "validation": { + "required": "Required", + "too_short": "Too short", + "too_long": "Too long", + "password_confirm": "Password and password confirmation must match" + }, "ok": "OK", "cancel": "Cancel", + "share": "Share", "unshare": "Unshare", - "close": "Close" + "close": "Close", + "add": "Add", + "open": "Open", + "see": "See", + "save": "Save", + "export": "Export", + "edit": "Edit", + "delete": "Delete", + "loading": "Loading...", + "send": "Send" } \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/partial/docs/directive.acledit.html b/docs-web/src/main/webapp/src/partial/docs/directive.acledit.html index 9cf0c792..2f542bcc 100644 --- a/docs-web/src/main/webapp/src/partial/docs/directive.acledit.html +++ b/docs-web/src/main/webapp/src/partial/docs/directive.acledit.html @@ -1,8 +1,8 @@
- - + + @@ -19,14 +19,14 @@
ForPermission{{ 'directive.acledit.acl_target' | translate }}{{ 'directive.acledit.acl_permission' | translate }}
-

Add a permission

+

{{ 'directive.acledit.add_permission' | translate }}

- +
@@ -39,11 +39,11 @@
- +
@@ -52,7 +52,7 @@
diff --git a/docs-web/src/main/webapp/src/partial/docs/directive.auditlog.html b/docs-web/src/main/webapp/src/partial/docs/directive.auditlog.html index 3a15eba9..e8527c54 100644 --- a/docs-web/src/main/webapp/src/partial/docs/directive.auditlog.html +++ b/docs-web/src/main/webapp/src/partial/docs/directive.auditlog.html @@ -10,9 +10,9 @@ {{ log.class }} - created - updated - deleted + {{ 'directive.auditlog.log_created' | translate }} + {{ 'directive.auditlog.log_updated' | translate }} + {{ 'directive.auditlog.log_deleted' | translate }} @@ -21,11 +21,11 @@ {{ log.message }} - Open - Open + {{ 'open' | translate }} + {{ 'open' | translate }} - See + {{ 'see' | translate }} {{ log.message }} diff --git a/docs-web/src/main/webapp/src/partial/docs/directive.selectrelation.html b/docs-web/src/main/webapp/src/partial/docs/directive.selectrelation.html index 82f8a9c3..933bb65e 100644 --- a/docs-web/src/main/webapp/src/partial/docs/directive.selectrelation.html +++ b/docs-web/src/main/webapp/src/partial/docs/directive.selectrelation.html @@ -6,7 +6,7 @@ -
\ No newline at end of file diff --git a/docs-web/src/main/webapp/src/partial/docs/directive.selecttag.html b/docs-web/src/main/webapp/src/partial/docs/directive.selecttag.html index 9f28bdc7..140b8224 100644 --- a/docs-web/src/main/webapp/src/partial/docs/directive.selecttag.html +++ b/docs-web/src/main/webapp/src/partial/docs/directive.selecttag.html @@ -6,6 +6,6 @@ -
\ No newline at end of file diff --git a/docs-web/src/main/webapp/src/partial/docs/document.default.html b/docs-web/src/main/webapp/src/partial/docs/document.default.html index dd0a6aca..5b12dc1b 100644 --- a/docs-web/src/main/webapp/src/partial/docs/document.default.html +++ b/docs-web/src/main/webapp/src/partial/docs/document.default.html @@ -2,9 +2,9 @@
-

Quick upload

+

{{ 'document.default.quick_upload' | translate }}

+ ng-multiple="true" allow-dir="false" ng-file-change="fileDropped($files, $event, $rejectedFiles)">
@@ -12,7 +12,7 @@
- +
@@ -33,28 +33,28 @@

- Drag & drop files here to upload + {{ 'document.default.drop_zone' | translate }}

- +
-

Latest activity

+

{{ 'document.default.latest_activity' | translate }}

    -
  • Crafted with by Sismics
  • -
  • API Documentation
  • -
  • Version: {{ app.current_version }}
  • -
  • Memory: {{ app.free_memory / 1000000 | number: 0 }}/{{ app.total_memory / 1000000 | number: 0 }} MB
  • +
  • +
  • {{ 'document.default.api_documentation' | translate }}
  • +
  • {{ 'document.default.version' | translate }} {{ app.current_version }}
  • +
  • {{ 'document.default.memory' | translate }} {{ app.free_memory / 1000000 | number: 0 }}/{{ app.total_memory / 1000000 | number: 0 }} MB
\ No newline at end of file diff --git a/docs-web/src/main/webapp/src/partial/docs/document.edit.html b/docs-web/src/main/webapp/src/partial/docs/document.edit.html index a75fdc68..00437134 100644 --- a/docs-web/src/main/webapp/src/partial/docs/document.edit.html +++ b/docs-web/src/main/webapp/src/partial/docs/document.edit.html @@ -3,32 +3,32 @@
- Primary metadata + {{ 'document.edit.primary_metadata' | translate }}
- +
- +
-
- +
- +
+ placeholder="{{ 'document.edit.subject_placeholder' | translate }}" name="subject" ng-model="document.subject" ng-disabled="fileIsUploading" />
- +
+ placeholder="{{ 'document.edit.identifier_placeholder' | translate }}" name="identifier" ng-model="document.identifier" ng-disabled="fileIsUploading" />
- +
+ placeholder="{{ 'document.edit.publisher_placeholder' | translate }}" name="publisher" ng-model="document.publisher" ng-disabled="fileIsUploading" />
- +
+ placeholder="{{ 'document.edit.format_placeholder' | translate }}" name="format" ng-model="document.format" ng-disabled="fileIsUploading" />
- +
+ placeholder="{{ 'document.edit.source_placeholder' | translate }}" name="source" ng-model="document.source" ng-disabled="fileIsUploading" />
- +
@@ -124,7 +125,7 @@
- +
+ @@ -40,9 +40,9 @@ - - - + + + @@ -54,15 +54,15 @@ @@ -79,22 +79,21 @@
- +
-
- {{ userInfo.storage_current / 1000000 | number: 0 }}MB ({{ userInfo.storage_current / userInfo.storage_quota * 100 | number: 1 }}%) - used on {{ userInfo.storage_quota / 1000000 | number: 0 }}MB +
+ {{ 'document.quota' | translate: '{ current: userInfo.storage_current / 1000000, percent: userInfo.storage_current / userInfo.storage_quota * 100, total: userInfo.storage_quota / 1000000 }' }}
- {{ totalDocuments }} document{{ totalDocuments > 1 ? 's' : '' }} found + {{ 'document.count' | translate: '{ count: totalDocuments }' }}  
diff --git a/docs-web/src/main/webapp/src/partial/docs/document.pdf.html b/docs-web/src/main/webapp/src/partial/docs/document.pdf.html index 2e1d60cb..1c844efb 100644 --- a/docs-web/src/main/webapp/src/partial/docs/document.pdf.html +++ b/docs-web/src/main/webapp/src/partial/docs/document.pdf.html @@ -1,5 +1,5 @@ - +
-
mm
+
{{ 'document.pdf.millimeter' | translate }}
\ No newline at end of file diff --git a/docs-web/src/main/webapp/src/partial/docs/document.share.html b/docs-web/src/main/webapp/src/partial/docs/document.share.html index 48bdbfad..dc0ca994 100644 --- a/docs-web/src/main/webapp/src/partial/docs/document.share.html +++ b/docs-web/src/main/webapp/src/partial/docs/document.share.html @@ -1,15 +1,15 @@ \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/partial/docs/document.view.activity.html b/docs-web/src/main/webapp/src/partial/docs/document.view.activity.html index ff100f5e..681ecdde 100644 --- a/docs-web/src/main/webapp/src/partial/docs/document.view.activity.html +++ b/docs-web/src/main/webapp/src/partial/docs/document.view.activity.html @@ -1,3 +1,3 @@ -

Every actions on this document are logged here.

+

{{ 'document.view.activity.message' | translate }}

\ No newline at end of file diff --git a/docs-web/src/main/webapp/src/partial/docs/document.view.content.html b/docs-web/src/main/webapp/src/partial/docs/document.view.content.html index a19f2dd4..cfd09e11 100644 --- a/docs-web/src/main/webapp/src/partial/docs/document.view.content.html +++ b/docs-web/src/main/webapp/src/partial/docs/document.view.content.html @@ -1,22 +1,22 @@

-
Subject
+
{{ 'document.subject' | translate }}
{{ document.subject }}
-
Identifier
+
{{ 'document.identifier' | translate }}
{{ document.identifier }}
-
Publisher
+
{{ 'document.publisher' | translate }}
{{ document.publisher }}
-
Format
+
{{ 'document.format' | translate }}
{{ document.format }}
-
Source
+
{{ 'document.source' | translate }}
{{ document.source }}
-
Type
+
{{ 'document.type' | translate }}
{{ document.type }}
-
Coverage
+
{{ 'document.coverage' | translate }}
{{ document.coverage }}
-
rights
+
{{ 'document.rights' | translate }}
{{ document.rights }}
-
Contributors
+
{{ 'document.contributors' | translate }}
@@ -26,7 +26,7 @@
-
Relations
+
{{ 'document.relations' | translate }}
@@ -70,7 +70,7 @@

- Drag & drop files here to upload + {{ 'document.view.content.drop_zone' | translate }}

diff --git a/docs-web/src/main/webapp/src/partial/docs/document.view.html b/docs-web/src/main/webapp/src/partial/docs/document.view.html index f6f27bb2..7e015bde 100644 --- a/docs-web/src/main/webapp/src/partial/docs/document.view.html +++ b/docs-web/src/main/webapp/src/partial/docs/document.view.html @@ -3,11 +3,11 @@

- Document not found + {{ 'document.view.not_found' | translate }}

- Access forbidden + {{ 'document.view.forbidden' | translate }}

@@ -16,25 +16,25 @@ @@ -42,12 +42,12 @@

{{ document.title }} {{ document.create_date | date: 'yyyy-MM-dd' }} - by {{ document.creator }} + {{ 'document.view.by_creator' | translate }} {{ document.creator }}

@@ -69,17 +69,17 @@

@@ -88,14 +88,14 @@

-

Loading...

-

No comments on this document yet

-

Error loading comments

+

{{ 'loading' | translate }}

+

{{ 'document.view.no_comments' | translate }}

+

{{ 'document.view.error_loading_comments' | translate }}

@@ -109,15 +109,15 @@ {{ comment.create_date | date: 'yyyy-MM-dd' }} Delete + ng-click="deleteComment(comment)">{{ 'delete' | translate }}

- - + +
diff --git a/docs-web/src/main/webapp/src/partial/docs/document.view.permissions.html b/docs-web/src/main/webapp/src/partial/docs/document.view.permissions.html index 701387a5..6ad27cef 100644 --- a/docs-web/src/main/webapp/src/partial/docs/document.view.permissions.html +++ b/docs-web/src/main/webapp/src/partial/docs/document.view.permissions.html @@ -1,7 +1,7 @@ -

Permissions can be applied directly to this document, or can come from tags.

+

-

Permissions on this document

+

{{ 'document.view.permissions.title' | translate }}

-

Permissions inherited by tags

+

{{ 'document.view.permissions.inherited_tags' | translate }}

-
Title Creation date {{ 'document.title' | translate }} {{ 'document.creation_date' | translate }}
- No document in the database - No matches for "{{ search }}" + {{ 'document.no_documents' | translate }} +
{{ document.title }} ({{ document.file_count }}) - + {{ document.create_date | date: 'yyyy-MM-dd' }}
+
- - - + + + @@ -28,8 +28,8 @@ diff --git a/docs-web/src/main/webapp/src/partial/docs/file.view.html b/docs-web/src/main/webapp/src/partial/docs/file.view.html index 8bf1cdb1..c78062f7 100644 --- a/docs-web/src/main/webapp/src/partial/docs/file.view.html +++ b/docs-web/src/main/webapp/src/partial/docs/file.view.html @@ -6,8 +6,8 @@
- - + +
@@ -28,6 +28,6 @@ ng-show="!error" />

- 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..1568b08d 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,20 @@

{{ 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/main.html b/docs-web/src/main/webapp/src/partial/docs/main.html index 1dacc154..c5898555 100644 --- a/docs-web/src/main/webapp/src/partial/docs/main.html +++ b/docs-web/src/main/webapp/src/partial/docs/main.html @@ -1,3 +1,3 @@
-

Loading...

+

{{ 'loading' | translate }}

\ No newline at end of file diff --git a/docs-web/src/main/webapp/src/partial/docs/settings.account.html b/docs-web/src/main/webapp/src/partial/docs/settings.account.html index 2b6739b5..7bf6e704 100644 --- a/docs-web/src/main/webapp/src/partial/docs/settings.account.html +++ b/docs-web/src/main/webapp/src/partial/docs/settings.account.html @@ -1,34 +1,34 @@

User account

- +
+ ng-minlength="8" ng-maxlength="50" placeholder="{{ 'settings.account.password' | translate }}" ng-model="user.password" />
- Required - Too short - Too long + {{ 'validation.required' | translate }} + {{ 'validation.too_short' | translate }} + {{ 'validation.too_long' | translate }}
- +
+ placeholder="{{ 'settings.account.password_confirm' | translate }}" ng-model="user.passwordconfirm" />
- Password and password confirmation must match + {{ 'validation.password_confirm' | translate }}
diff --git a/docs-web/src/main/webapp/src/partial/docs/settings.config.html b/docs-web/src/main/webapp/src/partial/docs/settings.config.html index b909bbe1..8b913897 100644 --- a/docs-web/src/main/webapp/src/partial/docs/settings.config.html +++ b/docs-web/src/main/webapp/src/partial/docs/settings.config.html @@ -1,22 +1,20 @@

- Guest access + {{ app.guest_login ? 'Enabled' : 'Disabled' }}

-

- Guest access is a mode where anyone can access {{ appName }} without password.
- Like a normal user, the guest user can only access its documents and those accessible through permissions.
+

- - + +
-

Theme customization

+

- +
@@ -24,7 +22,7 @@
- +
    @@ -32,15 +30,15 @@
- +
-
- +
- +
@@ -71,7 +69,7 @@

- Uploading the image... + {{ 'settings.config.uploading_image' | translate }}

diff --git a/docs-web/src/main/webapp/src/partial/docs/settings.default.html b/docs-web/src/main/webapp/src/partial/docs/settings.default.html index 52b633af..e69de29b 100644 --- a/docs-web/src/main/webapp/src/partial/docs/settings.default.html +++ b/docs-web/src/main/webapp/src/partial/docs/settings.default.html @@ -1 +0,0 @@ -

Settings

diff --git a/docs-web/src/main/webapp/src/style/main.less b/docs-web/src/main/webapp/src/style/main.less index ee0ecd83..070ca65f 100644 --- a/docs-web/src/main/webapp/src/style/main.less +++ b/docs-web/src/main/webapp/src/style/main.less @@ -113,6 +113,11 @@ } // File thumbnails +.thumbnail input[type="checkbox"] { + width: 26px; + height: 26px; +} + .thumbnail-file { cursor: pointer; } @@ -132,6 +137,13 @@ display: block; } +// Permissions table +.table-permissions { + .label-acl { + margin-right: 6px; + } +} + // Fields bound to datepicker input[readonly][datepicker-popup] { cursor: pointer; @@ -249,6 +261,7 @@ input[readonly].share-link { // Login .login-box-container { background: url('../../api/theme/image/background') no-repeat center; + background-size: cover; } .login-box { From 4822b8bf23c33d6f16d1fb9bee8bf29e03f7e2e9 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Wed, 1 Nov 2017 19:50:42 +0100 Subject: [PATCH 024/288] Update README.md --- README.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/README.md b/README.md index 0228e56a..2172015a 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,6 @@ Sismics Docs [![Build Status](https://secure.travis-ci.org/sismics/docs.png)](http://travis-ci.org/sismics/docs) ============ -_Web interface_ - -![Web interface](http://sismics.com/docs/screenshot1.png) - -_Android application_ - -![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) - What is Docs? --------------- From e49d00294163cc4c79d1201c72247e4a529ef657 Mon Sep 17 00:00:00 2001 From: bgamard Date: Thu, 2 Nov 2017 15:39:50 +0100 Subject: [PATCH 025/288] #111: translate templates --- .../controller/settings/SettingsSecurity.js | 2 +- docs-web/src/main/webapp/src/locale/en.json | 125 +++++++++++++++++- .../src/partial/docs/directive.acledit.html | 2 +- .../src/partial/docs/directive.auditlog.html | 2 +- .../src/partial/docs/settings.group.edit.html | 37 +++--- .../src/partial/docs/settings.group.html | 7 +- .../webapp/src/partial/docs/settings.html | 20 +-- .../webapp/src/partial/docs/settings.log.html | 8 +- .../docs/settings.security.disabletotp.html | 10 +- .../src/partial/docs/settings.security.html | 32 ++--- .../src/partial/docs/settings.session.html | 17 ++- .../src/partial/docs/settings.user.edit.html | 73 +++++----- .../src/partial/docs/settings.user.html | 9 +- .../src/partial/docs/settings.vocabulary.html | 14 +- .../webapp/src/partial/docs/tag.default.html | 12 +- .../webapp/src/partial/docs/tag.edit.html | 12 +- .../src/main/webapp/src/partial/docs/tag.html | 10 +- .../webapp/src/partial/docs/user.profile.html | 14 +- .../src/partial/docs/usergroup.default.html | 4 +- .../webapp/src/partial/docs/usergroup.html | 6 +- docs-web/src/main/webapp/src/style/main.less | 5 +- 21 files changed, 263 insertions(+), 158 deletions(-) diff --git a/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsSecurity.js b/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsSecurity.js index 91d007c9..27946f37 100644 --- a/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsSecurity.js +++ b/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsSecurity.js @@ -12,7 +12,7 @@ angular.module('docs').controller('SettingsSecurity', function($scope, User, $di * Enable TOTP. */ $scope.enableTotp = function() { - var title = $translate.instant('settings.security.enable_totp_title'); + var title = $translate.instant('settings.security.enable_totp'); var msg = $translate.instant('settings.security.enable_totp_message'); var btns = [ { result:'cancel', label: $translate.instant('cancel') }, diff --git a/docs-web/src/main/webapp/src/locale/en.json b/docs-web/src/main/webapp/src/locale/en.json index 17cc5573..61d8b4fe 100644 --- a/docs-web/src/main/webapp/src/locale/en.json +++ b/docs-web/src/main/webapp/src/locale/en.json @@ -143,34 +143,117 @@ } }, "tag": { + "new_tag": "New tag", + "search": "Search", + "edit_tag": "Edit tag", + "default": { + "title": "Tags", + "message_1": "Tags are labels associated to documents.", + "message_2": "A document can be tagged by multiple tags, and a tag can be applied to multiple documents.", + "message_3": "Using the button, you can edit permissions on a tag.", + "message_4": "If a tag can be read by another user or group, associated documents can also be read by those people.", + "message_5": "For example, tag your company documents with a tag MyCompany and add the permission Read to a group employees" + }, "edit": { "delete_tag_title": "Delete tag", - "delete_tag_message": "Do you really want to delete this tag?" + "delete_tag_message": "Do you really want to delete this tag?", + "name": "Name", + "color": "Color", + "parent": "Parent", + "info": "Permissions on this tag will also be applied to documents tagged {{ name }}" } }, "group": { - "rofile": { + "profile": { "members": "Members", "no_members": "No member", "related_links": "Related links", "edit_group": "Edit {{ name }} group" } }, + "user": { + "profile": { + "groups": "Groups", + "quota_used": "Quota used", + "percent_used": "{{ percent | number: 0 }}% Used", + "related_links": "Related links", + "document_created": "Documents created by {{ username }}", + "edit_user": "Edit {{ username }} user" + } + }, + "usergroup": { + "search_groups": "Search in groups", + "search_users": "Search in users", + "you": "It's you!", + "default": { + "title": "Users & Groups", + "message": "Here you can view informations about users and groups." + } + }, "settings": { + "menu_personal_settings": "Personal settings", + "menu_user_account": "User account", + "menu_two_factor_auth": "Two-factor authentication", + "menu_opened_sessions": "Opened sessions", + "menu_general_settings": "General settings", + "menu_users": "Users", + "menu_groups": "Groups", + "menu_vocabularies": "Vocabularies", + "menu_configuration": "Configuration", + "menu_server_logs": "Server logs", "user": { + "title": "Users management", + "add_user": "Add a user", + "username": "Username", + "create_date": "Create date", "edit": { "delete_user_title": "Delete user", - "delete_user_message": "Do you really want to delete this user? All associated documents, files and tags will be deleted" + "delete_user_message": "Do you really want to delete this user? All associated documents, files and tags will be deleted", + "edit_user_title": "Edit \"{{ username }}\"", + "add_user_title": "Add user", + "username": "Username", + "email": "E-mail", + "groups": "Groups", + "storage_quota": "Storage quota", + "storage_quota_placeholder": "Storage quota (in MB)", + "password": "Password", + "password_confirm": "Password (confirm)" } }, "security": { - "enable_totp_title": "Enable two-factor authentication", - "enable_totp_message": "Make sure you have a TOTP-compatible application on your phone ready to add a new account" + "enable_totp": "Enable two-factor authentication", + "enable_totp_message": "Make sure you have a TOTP-compatible application on your phone ready to add a new account", + "title": "Two-factor authentication", + "message_1": "Two-factor authentication allows you to add a layer of security on your {{ appName }} account.
Before activating this feature, make sure you have a TOTP-compatible app on your phone:", + "message_google_authenticator": "For Android, iOS, and Blackberry: Google Authenticator", + "message_duo_mobile": "For Android and iOS: Duo Mobile", + "message_authenticator": "For Windows Phone: Authenticator", + "message_2": "Those applications automatically generate a validation code that changes after a certain period of time.
You will be required to enter this validation code each time you login on {{ appName }}.", + "secret_key": "Your secret key is: {{ secret }}", + "secret_key_warning": "Configure your TOTP app on your phone with this secret key now, you will not be able to access it later.", + "totp_enabled_message": "Two-factor authentication is enabled on your account.
Each time you login on {{ appName }}, you will be asked a validation code from your configured phone app.
If you loose your phone, you will not be able to login into your account but active sessions will allow you to regenerate a secrey key.", + "disable_totp": { + "disable_totp": "Disable two-factor authentication", + "message": "Your account will not be protected by the two-factor authentication anymore.", + "confirm_password": "Confirm your password", + "submit": "Disable two-factor authentication" + } }, "group": { + "title": "Groups management", + "add_group": "Add a group", + "name": "Name", "edit": { "delete_group_title": "Delete group", - "delete_group_message": "Do you really want to delete this group?" + "delete_group_message": "Do you really want to delete this group?", + "edit_group_title": "Edit \"{{ name }}\"", + "add_group_title": "Add group", + "name": "Name", + "parent_group": "Parent group", + "search_group": "Search a group", + "members": "Members", + "new_member": "New member", + "search_user": "Search a user" } }, "account": { @@ -191,6 +274,31 @@ "logo": "Logo (squared size)", "background_image": "Background image", "uploading_image": "Uploading the image..." + }, + "log": { + "title": "Server logs", + "date": "Date", + "tag": "Tag", + "message": "Message" + }, + "session": { + "title": "Opened sessions", + "created_date": "Created date", + "last_connection_date": "Last connection date", + "user_agent": "From", + "current": "Current", + "current_session": "This is the current session", + "clear_message": "All other devices connected to this account will be disconnected", + "clear": "Clear all other sessions" + }, + "vocabulary": { + "title": "Vocabulary entries", + "choose_vocabulary": "Choose a vocabulary to edit", + "type": "Type", + "coverage": "Coverage", + "rights": "Rights", + "value": "Value", + "order": "Order" } }, "directive": { @@ -230,7 +338,10 @@ "required": "Required", "too_short": "Too short", "too_long": "Too long", - "password_confirm": "Password and password confirmation must match" + "email": "Must be a valid e-mail", + "password_confirm": "Password and password confirmation must match", + "number": "Number required", + "no_space": "Space are not allowed" }, "ok": "OK", "cancel": "Cancel", diff --git a/docs-web/src/main/webapp/src/partial/docs/directive.acledit.html b/docs-web/src/main/webapp/src/partial/docs/directive.acledit.html index 2f542bcc..83cb6051 100644 --- a/docs-web/src/main/webapp/src/partial/docs/directive.acledit.html +++ b/docs-web/src/main/webapp/src/partial/docs/directive.acledit.html @@ -9,7 +9,7 @@
FromForPermission{{ 'document.view.permissions.acl_source' | translate }}{{ 'document.view.permissions.acl_target' | translate }}{{ 'document.view.permissions.acl_permission' | translate }}
- - {{ a.perm }} + + {{ 'acl.' + a.perm | translate }}
- {{ a.perm }} + {{ 'acl.' + a.perm | translate }} diff --git a/docs-web/src/main/webapp/src/partial/docs/directive.auditlog.html b/docs-web/src/main/webapp/src/partial/docs/directive.auditlog.html index e8527c54..61794c1a 100644 --- a/docs-web/src/main/webapp/src/partial/docs/directive.auditlog.html +++ b/docs-web/src/main/webapp/src/partial/docs/directive.auditlog.html @@ -28,7 +28,7 @@ {{ 'see' | translate }} - {{ log.message }} + {{ 'acl.' + log.message | translate }} {{ log.message }} diff --git a/docs-web/src/main/webapp/src/partial/docs/settings.group.edit.html b/docs-web/src/main/webapp/src/partial/docs/settings.group.edit.html index 1a49d88c..221dbedc 100644 --- a/docs-web/src/main/webapp/src/partial/docs/settings.group.edit.html +++ b/docs-web/src/main/webapp/src/partial/docs/settings.group.edit.html @@ -1,33 +1,29 @@
-

Edit - "{{ group.name }}" -

-

Add - group -

+

+

+
- - +
+ ng-minlength="3" ng-maxlength="50" placeholder="{{ 'settings.group.edit.name' | translate }}" ng-model="group.name"/>
- Required - Too short - Too long + {{ 'validation.required' | translate }} + {{ 'validation.too_short' | translate }} + {{ 'validation.too_long' | translate }}
- +
@@ -35,29 +31,30 @@
-

Members

+

{{ 'settings.group.edit.members' | translate }}

- +
-
- +
Groups management Add a group +

+ + {{ 'settings.group.add_group' | translate }} +

- + diff --git a/docs-web/src/main/webapp/src/partial/docs/settings.html b/docs-web/src/main/webapp/src/partial/docs/settings.html index 6c574a65..a022d7b2 100644 --- a/docs-web/src/main/webapp/src/partial/docs/settings.html +++ b/docs-web/src/main/webapp/src/partial/docs/settings.html @@ -1,22 +1,22 @@
diff --git a/docs-web/src/main/webapp/src/partial/docs/settings.log.html b/docs-web/src/main/webapp/src/partial/docs/settings.log.html index 32769daa..f6843bed 100644 --- a/docs-web/src/main/webapp/src/partial/docs/settings.log.html +++ b/docs-web/src/main/webapp/src/partial/docs/settings.log.html @@ -1,10 +1,10 @@ -

Server logs

+

Name{{ 'settings.group.name' | translate }}
- - - + + + diff --git a/docs-web/src/main/webapp/src/partial/docs/settings.security.disabletotp.html b/docs-web/src/main/webapp/src/partial/docs/settings.security.disabletotp.html index 9d13d383..8a41aef9 100644 --- a/docs-web/src/main/webapp/src/partial/docs/settings.security.disabletotp.html +++ b/docs-web/src/main/webapp/src/partial/docs/settings.security.disabletotp.html @@ -1,18 +1,18 @@ \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/partial/docs/settings.security.html b/docs-web/src/main/webapp/src/partial/docs/settings.security.html index eaa75515..33ed422d 100644 --- a/docs-web/src/main/webapp/src/partial/docs/settings.security.html +++ b/docs-web/src/main/webapp/src/partial/docs/settings.security.html @@ -1,43 +1,33 @@

- Two-factor authentication + {{ user.totp_enabled ? 'Enabled' : 'Disabled' }}

-

- Two-factor authentication allows you to add a layer of security on your {{ appName }} account.
- Before activating this feature, make sure you have a TOTP-compatible app on your phone: -

+

+

- Those applications automatically generate a validation code that changes after a certain period of time.
- You will be required to enter this validation code each time you login on {{ appName }}. -

-

- +

-

Your secret key is: {{ secret }}

+

- Configure your TOTP app on your phone with this secret key now, you will not be able to access it later. + {{ 'settings.security.secret_key_warning' | translate }}

+

- Two-factor authentication is enabled on your account.
- Each time you login on {{ appName }}, you will be asked a validation code from your configured phone app.
- If you loose your phone, you will not be able to login into your account but active sessions will allow you to regenerate a secrey key. -

-

- +

\ No newline at end of file diff --git a/docs-web/src/main/webapp/src/partial/docs/settings.session.html b/docs-web/src/main/webapp/src/partial/docs/settings.session.html index 2b5c015b..e7da00d7 100644 --- a/docs-web/src/main/webapp/src/partial/docs/settings.session.html +++ b/docs-web/src/main/webapp/src/partial/docs/settings.session.html @@ -1,11 +1,11 @@ -

Opened sessions

+

DateTagMessage{{ 'settings.log.date' | translate }}{{ 'settings.log.tag' | translate }}{{ 'settings.log.message' | translate }}
- - - - + + + + @@ -14,11 +14,14 @@
Created dateLast connection dateFromCurrent{{ 'settings.session.created_date' | translate }}{{ 'settings.session.last_connection_date' | translate }}{{ 'settings.session.user_agent' | translate }}{{ 'settings.session.current' | translate }}
{{ session.last_connection_date | date: 'yyyy-MM-dd HH:mm' }} {{ session.ip }} - +
- +
\ No newline at end of file diff --git a/docs-web/src/main/webapp/src/partial/docs/settings.user.edit.html b/docs-web/src/main/webapp/src/partial/docs/settings.user.edit.html index 88d44636..69d98f96 100644 --- a/docs-web/src/main/webapp/src/partial/docs/settings.user.edit.html +++ b/docs-web/src/main/webapp/src/partial/docs/settings.user.edit.html @@ -1,45 +1,41 @@
-

Edit - "{{ user.username }}" -

-

Add - user -

+

+

+
- - +
+ ng-minlength="3" ng-maxlength="50" placeholder="{{ 'settings.user.edit.username' | translate }}" ng-model="user.username"/>
- Required - Too short - Too long + {{ 'validation.required' | translate }} + {{ 'validation.too_short' | translate }} + {{ 'validation.too_long' | translate }}
+
- - +
+ ng-minlength="1" ng-maxlength="100" placeholder="{{ 'settings.user.edit.username' | translate }}" ng-model="user.email"/>
- Required - Must be a valid e-mail - Too short - Too long + {{ 'validation.required' | translate }} + {{ 'validation.email' | translate }} + {{ 'validation.too_short' | translate }} + {{ 'validation.too_long' | translate }}
-
- +
+
-
- +
+
-
MB
+ ng-pattern="/[0-9]*/" placeholder="{{ 'settings.user.edit.storage_quota_placeholder' | translate }}" ng-model="user.storage_quota"/> +
{{ 'filter.filesize.mb' | translate }}
- Number required + {{ 'validation.number' | translate }}
+
- - +
+ ng-minlength="8" ng-maxlength="50" placeholder="{{ 'settings.user.edit.password' | translate }}" ng-model="user.password"/>
- Required - Too short - Too long + {{ 'validation.required' | translate }} + {{ 'validation.too_short' | translate }} + {{ 'validation.too_long' | translate }}
+
- - +
+ placeholder="{{ 'settings.user.edit.password_confirm' | translate }}" ng-model="user.passwordconfirm"/>
- Password and password confirmation must match + {{ 'validation.password_confirm' | translate }}
+
diff --git a/docs-web/src/main/webapp/src/partial/docs/settings.user.html b/docs-web/src/main/webapp/src/partial/docs/settings.user.html index 2f4e991e..e0a9fb64 100644 --- a/docs-web/src/main/webapp/src/partial/docs/settings.user.html +++ b/docs-web/src/main/webapp/src/partial/docs/settings.user.html @@ -1,12 +1,15 @@ -

Users management Add a user

+

+ + {{ 'settings.user.add_user' | translate }} +

- - + + diff --git a/docs-web/src/main/webapp/src/partial/docs/settings.vocabulary.html b/docs-web/src/main/webapp/src/partial/docs/settings.vocabulary.html index a7a1e82a..7d6c871b 100644 --- a/docs-web/src/main/webapp/src/partial/docs/settings.vocabulary.html +++ b/docs-web/src/main/webapp/src/partial/docs/settings.vocabulary.html @@ -1,13 +1,13 @@ -

Vocabulary entries

+

- +
@@ -15,8 +15,8 @@
UsernameCreate date{{ 'settings.user.username' | translate }}{{ 'settings.user.create_date' | translate }}
- - + + diff --git a/docs-web/src/main/webapp/src/partial/docs/tag.default.html b/docs-web/src/main/webapp/src/partial/docs/tag.default.html index f9af3e0f..763a9872 100644 --- a/docs-web/src/main/webapp/src/partial/docs/tag.default.html +++ b/docs-web/src/main/webapp/src/partial/docs/tag.default.html @@ -1,6 +1,6 @@ -

Tags

-

Tags are labels associated to documents.

-

A document can be tagged by multiple tags, and a tag can be applied to multiple documents.

-

Using the button, you can edit permissions on a tag.

-

If a tag can be read by another user or group, associated documents can also be read by those people.

-

For example, tag your company documents with a tag MyCompany and add the permission Read to a group employees

\ No newline at end of file +

{{ 'tag.default.title' | translate }}

+

+

+

+

+

\ No newline at end of file diff --git a/docs-web/src/main/webapp/src/partial/docs/tag.edit.html b/docs-web/src/main/webapp/src/partial/docs/tag.edit.html index 398470b6..d8d56de7 100644 --- a/docs-web/src/main/webapp/src/partial/docs/tag.edit.html +++ b/docs-web/src/main/webapp/src/partial/docs/tag.edit.html @@ -1,5 +1,5 @@
- +
ValueOrder{{ 'settings.vocabulary.value' | translate }}{{ 'settings.vocabulary.order' | translate }}
@@ -25,7 +25,7 @@ {{ tag.name }} diff --git a/docs-web/src/main/webapp/src/partial/docs/user.profile.html b/docs-web/src/main/webapp/src/partial/docs/user.profile.html index b67c0f6c..96a9ac1d 100644 --- a/docs-web/src/main/webapp/src/partial/docs/user.profile.html +++ b/docs-web/src/main/webapp/src/partial/docs/user.profile.html @@ -2,34 +2,34 @@

{{ user.username }} {{ user.email }}

-

Groups

+

{{ 'user.profile.groups' | translate }}

-

Quota used

+

{{ 'user.profile.quota_used' | translate }}

-
+
- {{ (user.storage_current / user.storage_quota * 100) }}% Used + {{ 'user.profile.percent_used' | translate: '{ percent: user.storage_current / user.storage_quota * 100 }' }}
-

Related links

+

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

\ No newline at end of file diff --git a/docs-web/src/main/webapp/src/partial/docs/usergroup.default.html b/docs-web/src/main/webapp/src/partial/docs/usergroup.default.html index 5899b461..f53aee74 100644 --- a/docs-web/src/main/webapp/src/partial/docs/usergroup.default.html +++ b/docs-web/src/main/webapp/src/partial/docs/usergroup.default.html @@ -1,2 +1,2 @@ -

Users & Groups

-

Here you can view informations about users and groups.

\ No newline at end of file +

{{ 'usergroup.default.title' | translate }}

+

{{ 'usergroup.default.message' | translate }}

diff --git a/docs-web/src/main/webapp/src/partial/docs/usergroup.html b/docs-web/src/main/webapp/src/partial/docs/usergroup.html index 6e0000b1..1f169664 100644 --- a/docs-web/src/main/webapp/src/partial/docs/usergroup.html +++ b/docs-web/src/main/webapp/src/partial/docs/usergroup.html @@ -3,7 +3,7 @@

- +

- +
@@ -21,7 +21,7 @@

- +

@@ -31,7 +31,7 @@ diff --git a/docs-web/src/main/webapp/src/style/main.less b/docs-web/src/main/webapp/src/style/main.less index 070ca65f..95c27a17 100644 --- a/docs-web/src/main/webapp/src/style/main.less +++ b/docs-web/src/main/webapp/src/style/main.less @@ -35,11 +35,12 @@ // Documents list .table-documents { thead th { - cursor: pointer; + cursor: pointer; + white-space: nowrap; } tbody tr { - cursor: pointer; + cursor: pointer; } .cell-tags { From 54d5f1cb1b7313ec9ef93162d41ceda74179604e Mon Sep 17 00:00:00 2001 From: bgamard Date: Thu, 2 Nov 2017 17:14:34 +0100 Subject: [PATCH 026/288] #111: french translation --- docs-web/src/main/webapp/src/locale/en.json | 22 +- docs-web/src/main/webapp/src/locale/fr.json | 368 ++++++++++++++++++ .../src/partial/docs/directive.auditlog.html | 2 +- .../src/partial/docs/settings.config.html | 2 +- .../src/partial/docs/settings.vocabulary.html | 2 +- 5 files changed, 387 insertions(+), 9 deletions(-) diff --git a/docs-web/src/main/webapp/src/locale/en.json b/docs-web/src/main/webapp/src/locale/en.json index 61d8b4fe..b3c356b8 100644 --- a/docs-web/src/main/webapp/src/locale/en.json +++ b/docs-web/src/main/webapp/src/locale/en.json @@ -209,8 +209,8 @@ "edit": { "delete_user_title": "Delete user", "delete_user_message": "Do you really want to delete this user? All associated documents, files and tags will be deleted", - "edit_user_title": "Edit \"{{ username }}\"", - "add_user_title": "Add user", + "edit_user_title": "Edit \"{{ username }}\"", + "add_user_title": "Add a user", "username": "Username", "email": "E-mail", "groups": "Groups", @@ -298,7 +298,8 @@ "coverage": "Coverage", "rights": "Rights", "value": "Value", - "order": "Order" + "order": "Order", + "new_entry": "New entry" } }, "directive": { @@ -311,7 +312,14 @@ "auditlog": { "log_created": "created", "log_updated": "updated", - "log_deleted": "deleted" + "log_deleted": "deleted", + "Acl": "ACL", + "Comment": "Comment", + "Document": "Document", + "File": "File", + "Group": "Group", + "Tag": "Tag", + "User": "User" }, "selectrelation": { "typeahead": "Type a document title" @@ -341,7 +349,7 @@ "email": "Must be a valid e-mail", "password_confirm": "Password and password confirmation must match", "number": "Number required", - "no_space": "Space are not allowed" + "no_space": "Spaces are not allowed" }, "ok": "OK", "cancel": "Cancel", @@ -356,5 +364,7 @@ "edit": "Edit", "delete": "Delete", "loading": "Loading...", - "send": "Send" + "send": "Send", + "enabled": "Enabled", + "disabled": "Disabled" } \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/locale/fr.json b/docs-web/src/main/webapp/src/locale/fr.json index 7a73a41b..a07c9a90 100644 --- a/docs-web/src/main/webapp/src/locale/fr.json +++ b/docs-web/src/main/webapp/src/locale/fr.json @@ -1,2 +1,370 @@ { + "login": { + "username": "Nom d'utilisateur", + "password": "Mot de passe", + "validation_code_required": "Un code de validation est requis", + "validation_code_title": "Vous avez activé l'authentification en deux étapes sur votre compte. Veuillez entrer un code de validation généré par l'application mobile que vous avez configurée.", + "validation_code": "Code de validation", + "remember_me": "Se souvenir de moi", + "submit": "Connexion", + "login_as_guest": "Connexion en invité", + "login_failed_title": "Echec de connexion", + "login_failed_message": "Nom d'utilisateur ou mot de passe invalide" + }, + "index": { + "toggle_navigation": "Afficher/cacher la navigation", + "nav_documents": "Documents", + "nav_tags": "Tags", + "nav_users_groups": "Utilisateurs & Groupes", + "error_info": "{{ count }} nouvelle{{ count > 1 ? 's' : '' }} erreur{{ count > 1 ? 's' : '' }}", + "logged_as": "Connecté en tant que {{ username }}", + "nav_settings": "Paramètres", + "logout": "Déconnexion" + }, + "document": { + "add_document": "Ajouter un document", + "tags": "Tags", + "no_tags": "Aucun tag", + "no_documents": "Aucun document dans la base de données", + "search": "Rechercher", + "search_empty": "Aucun résultat pour \"{{ search }}\"", + "shared": "Partagé", + "title": "Titre", + "description": "Description", + "contributors": "Contributeurs", + "language": "Langue", + "creation_date": "Date de création", + "subject": "Sujet", + "identifier": "Identifiant", + "publisher": "Editeur", + "format": "Format", + "source": "Source", + "type": "Type", + "coverage": "Couverture", + "rights": "Droits", + "relations": "Relations", + "page_size": "Taille de page", + "page_size_10": "10 par page", + "page_size_20": "20 par page", + "page_size_30": "20 par page", + "upgrade_quota": "Pour augmenter votre quota, veuillez contacter votre administrateur", + "quota": "{{ current | number: 0 }}Mo ({{ percent | number: 1 }}%) utilisé sur {{ total | number: 0 }}MB", + "count": "{{ count }} document{{ count > 1 ? 's' : '' }} trouvé{{ count > 1 ? 's' : '' }}", + "view": { + "delete_comment_title": "Supprimer un commentaire", + "delete_comment_message": "Etes-vous sûr de vouloir supprimer ce commentaire ?", + "delete_document_title": "Supprimer un document", + "delete_document_message": "Etes-vous sûr de vouloir supprimer ce document ?", + "shared_document_title": "Document partagé", + "shared_document_message": "Vous pouvez partager ce document avec ce lien. Veuillez noter que toute personne ayant ce lien peut accéder au document.
", + "not_found": "Document introuvable", + "forbidden": "Accès non autorisé", + "download_files": "Télécharger les fichiers", + "export_pdf": "Exporter en PDF", + "by_creator": "par", + "comments": "Commentaires", + "no_comments": "Aucun commentaire sur ce document", + "add_comment": "Ajouter un commentaire", + "error_loading_comments": "Erreur au chargement des commentaires", + "content": { + "content": "Contenu", + "delete_file_title": "Supprimer un fichier", + "delete_file_message": "Etes-vous sûr de vouloir supprimer ce fichier ?", + "upload_pending": "En attente...", + "upload_progress": "Envoi...", + "upload_error": "Erreur d'envoi", + "upload_error_quota": "Quota atteint", + "drop_zone": "Glisser & déposer des fichiers ici pour les envoyer" + }, + "permissions": { + "permissions": "Permissions", + "message": "Les permissions peuvent être appliquées directement sur ce document, ou provenir de tags.", + "title": "Permissions sur ce document", + "inherited_tags": "Permissions héritées par tags", + "acl_source": "De", + "acl_target": "Pour", + "acl_permission": "Permission" + }, + "activity": { + "activity": "Activité", + "message": "Toutes les actions sur ce document sont consignées ici." + } + }, + "edit": { + "document_edited_with_errors": "Document modifié avec succès, mais certains fichiers n'ont pu être envoyés", + "document_added_with_errors": "Document ajouté avec succès, mais certains fichiers n'ont pu être envoyés", + "quota_reached": "Quota atteint", + "primary_metadata": "Métadonnées principales", + "title_placeholder": "La nature ou le genre de la ressource", + "description_placeholder": "Un résumé de la ressource", + "new_files": "Nouveaux fichiers", + "orphan_files": "+ {{ count }} fichier{{ count > 1 ? 's' : '' }}", + "additional_metadata": "Métadonnées secondaires", + "subject_placeholder": "Le sujet de la ressource", + "identifier_placeholder": "Une référence unique de la ressource dans un contexte donné", + "publisher_placeholder": "Une entité responsable de la mise à disposition de la ressource", + "format_placeholder": "Le format physique ou numérique de la ressource", + "source_placeholder": "Une ressource liée à cette ressource", + "uploading_files": "Envoi des fichiers..." + }, + "default": { + "upload_pending": "En attente...", + "upload_progress": "Envoi...", + "upload_error": "Erreur d'envoi", + "upload_error_quota": "Quota atteint", + "quick_upload": "Envoi rapide", + "drop_zone": "Glisser & déposer des fichiers ici pour les envoyer", + "add_new_document": "Ajouter à un nouveau document", + "latest_activity": "Activité récente", + "footer_sismics": "Conçu avec par Sismics", + "api_documentation": "Documentation API", + "version": "Version :", + "memory": "Mémoire :" + }, + "pdf": { + "export_title": "Exporter en PDF", + "export_metadata": "Exporter les métadonnées", + "export_comments": "Exporter les commentaires", + "fit_to_page": "Ajuster les images à la page", + "margin": "Marge", + "millimeter": "mm" + }, + "share": { + "title": "Partager un document", + "message": "Nommez le partage si vous souhaitez partager plusieurs fois le même document.", + "submit": "Partager" + } + }, + "file": { + "view": { + "previous": "Précédent", + "next": "Suivant", + "not_found": "Fichier introuvable" + } + }, + "tag": { + "new_tag": "Nouveau tag", + "search": "Rechercher", + "edit_tag": "Modifier le tag", + "default": { + "title": "Tags", + "message_1": "Les Tags sont des libellés associés aux documents.", + "message_2": "Un document peut être taggé par plusieurs tags, et un tag peut être appliqué à plusieurs documents.", + "message_3": "En utilisant le bouton , vous pouvez modifier les permissions sur un tag.", + "message_4": "Si un tag peut être lu par un autre utilisateur ou groupe, les documents associés peuvent également être lus par ces personnes.", + "message_5": "Par exemple, taggez les documents de votre entreprise avec un tag MonEntreprise et ajoutez la permission Lecture à un groupe employés" + }, + "edit": { + "delete_tag_title": "Supprimer un tag", + "delete_tag_message": "Etes-vous sûr de vouloir supprimer ce tag ?", + "name": "Nom", + "color": "Couleur", + "parent": "Parent", + "info": "Les permissions sur ce tag seront également appliquées aux documents taggés avec {{ name }}" + } + }, + "group": { + "profile": { + "members": "Membres", + "no_members": "Aucun membre", + "related_links": "Liens relatifs", + "edit_group": "Modifer le groupe {{ name }}" + } + }, + "user": { + "profile": { + "groups": "Groupes", + "quota_used": "Quota utilisé", + "percent_used": "{{ percent | number: 0 }}% utilisé", + "related_links": "Liens relatifs", + "document_created": "Documents créés par {{ username }}", + "edit_user": "Modifier l'utilisateur {{ username }}" + } + }, + "usergroup": { + "search_groups": "Rechercher dans les groupes", + "search_users": "Rechercher dans les utilisateurs", + "you": "C'est vous !", + "default": { + "title": "Utilisateurs & Groupes", + "message": "Vous pouvez consulter ici les informations sur les utilisateurs et les groupes." + } + }, + "settings": { + "menu_personal_settings": "Paramètres personnels", + "menu_user_account": "Compte utilisateur", + "menu_two_factor_auth": "Authentification en deux étapes", + "menu_opened_sessions": "Sessions ouvertes", + "menu_general_settings": "Paramètres généraux", + "menu_users": "Utilisateurs", + "menu_groups": "Groupes", + "menu_vocabularies": "Vocabulaires", + "menu_configuration": "Configuration", + "menu_server_logs": "Logs serveur", + "user": { + "title": "Gestion des utilisateurs", + "add_user": "Ajouter un utilisateur", + "username": "Nom d'utilisateur", + "create_date": "Date de création", + "edit": { + "delete_user_title": "Supprimer un utilisateur", + "delete_user_message": "Etes-vous sûr de vouloir supprimer cet utilisateur ? Tous les documents, fichiers et tags associés seront supprimés", + "edit_user_title": "Modifier \"{{ username }}\"", + "add_user_title": "Ajouter un utilisateur", + "username": "Nom d'utilisateur", + "email": "E-mail", + "groups": "Groupes", + "storage_quota": "Quota de stockage", + "storage_quota_placeholder": "Quota de stockage (en Mo)", + "password": "Mot de passe", + "password_confirm": "Mot de passe (confirmation)" + } + }, + "security": { + "enable_totp": "Activer l'authentification en deux étapes", + "enable_totp_message": "Assurez-vous d'avoir une application compatible TOTP sur votre téléphone prête à être configurée", + "title": "Authentification en deux étapes", + "message_1": "L'authentification en deux étapes vous permet d'ajouter une couche de sécurité supplémentaire sur votre compte {{ appName }}.
Avant d'activer cette fonctionnalité, assurez-vous d'avoir une application compatible TOTP sur votre téléphone :", + "message_google_authenticator": "Pour Android, iOS, et Blackberry: Google Authenticator", + "message_duo_mobile": "Pour Android et iOS: Duo Mobile", + "message_authenticator": "Pour Windows Phone: Authenticator", + "message_2": "Ces applications génèrent automatique un code de validation changeant après un intervalle de temps donné.
Il sera nécessaire d'entrer ce code de validation à chaque connexion à {{ appName }}.", + "secret_key": "Votre clé secrète est : {{ secret }}", + "secret_key_warning": "Configurez votre application TOTP sur votre téléphone avec cette clé secrète maintenant, elle ne sera plus disponible ensuite.", + "totp_enabled_message": "L'authentification en deux étapes est activée sur votre compte.
A chaque connexion sur {{ appName }}, un code de validation provenant de votre application mobile vous sera demandé.
Si vous perdez votre téléphone, il ne sera plus possible de vous connecter à votre compte, mais les sessions actives vous permettront de générer une nouvelle clé secrète.", + "disable_totp": { + "disable_totp": "Désactiver l'authentification en deux étapes", + "message": "Votre compte ne sera plus protégé par l'authentification en deux étapes.", + "confirm_password": "Confirmez votre mot de passe", + "submit": "Désactiver l'authentification en deux étapes" + } + }, + "group": { + "title": "Gestion des groupes", + "add_group": "Ajouter un groupe", + "name": "Nom", + "edit": { + "delete_group_title": "Supprimer un groupe", + "delete_group_message": "Etes-vous sûr de vouloir supprimer ce groupe ?", + "edit_group_title": "Modifier \"{{ name }}\"", + "add_group_title": "Ajouter un groupe", + "name": "Nom", + "parent_group": "Groupe parent", + "search_group": "Rechercher un groupe", + "members": "Membres", + "new_member": "Nouveau membre", + "search_user": "Rechercher un utilisateur" + } + }, + "account": { + "password": "Mot de passe", + "password_confirm": "Mot de passe (confirmation)", + "updated": "Compte mis à jout avec succès" + }, + "config": { + "title_guest_access": "Accès invité", + "message_guest_access": "L'accès invité est un mode dans lequel quiconque peut accéder à {{ appName }} sans mot de passe.
Comme un utilisateur normal, l'invité ne pourra accéder qu'aux documents auquel il a accès via les permissions.
", + "enable_guest_access": "Activer l'accès invité", + "disable_guest_access": "Désactiver l'accès invité", + "title_theme": "Personnalisation de l'interface", + "application_name": "Nom de l'application", + "main_color": "Couleur principale", + "custom_css": "CSS personnalisée", + "custom_css_placeholder": "CSS personnalisée ajoutée après la feuille de style principale", + "logo": "Logo (Taille carrée)", + "background_image": "Image de fond", + "uploading_image": "Envoi de l'image..." + }, + "log": { + "title": "Logs serveur", + "date": "Date", + "tag": "Tag", + "message": "Message" + }, + "session": { + "title": "Sessions ouvertes", + "created_date": "Date de création", + "last_connection_date": "Date de dernière connexion", + "user_agent": "Depuis", + "current": "Courante", + "current_session": "Ceci est la session courante", + "clear_message": "Tous les autres appareils connectés à ce compte seront déconnectés", + "clear": "Fermeture des autres sessions" + }, + "vocabulary": { + "title": "Entrées de vocabulaire", + "choose_vocabulary": "Choisissez un vocabulaire à modifier", + "type": "Type", + "coverage": "Couverture", + "rights": "Droits", + "value": "Valeur", + "order": "Ordre", + "new_entry": "Nouvelle entrée" + } + }, + "directive": { + "acledit": { + "acl_target": "Pour", + "acl_permission": "Permission", + "add_permission": "Ajouter une permission", + "search_user_group": "Rechercher un utilisateur ou un groupe" + }, + "auditlog": { + "log_created": "créé", + "log_updated": "mis à jour", + "log_deleted": "supprimé", + "Acl": "ACL", + "Comment": "Commentaire", + "Document": "Document", + "File": "Fichier", + "Group": "Groupe", + "Tag": "Tag", + "User": "Utilisateur" + }, + "selectrelation": { + "typeahead": "Entrez un titre de document" + }, + "selecttag": { + "typeahead": "Entrez un tag" + } + }, + "filter": { + "filesize": { + "mb": "Mo", + "kb": "Ko" + } + }, + "acl": { + "READ": "Lecture", + "READWRITE": "Ecriture", + "WRITE": "Ecriture", + "USER": "Utilisateur", + "GROUP": "Groupe", + "SHARE": "Partage" + }, + "validation": { + "required": "Requis", + "too_short": "Trop court", + "too_long": "Trop long", + "email": "Doit être une adresse e-mail valide", + "password_confirm": "Le mot de passe et sa confirmation doivent être identiques", + "number": "Nombre requis", + "no_space": "Les espaces ne sont pas autorisés" + }, + "ok": "OK", + "cancel": "Annuler", + "share": "Partager", + "unshare": "Départager", + "close": "Fermer", + "add": "Ajouter", + "open": "Ouvrir", + "see": "Voir", + "save": "Enregistrer", + "export": "Exporter", + "edit": "Modifier", + "delete": "Supprimer", + "loading": "Chargement...", + "send": "Envoyer", + "enabled": "Activé", + "disabled": "Désactivé" } \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/partial/docs/directive.auditlog.html b/docs-web/src/main/webapp/src/partial/docs/directive.auditlog.html index 61794c1a..3834cf52 100644 --- a/docs-web/src/main/webapp/src/partial/docs/directive.auditlog.html +++ b/docs-web/src/main/webapp/src/partial/docs/directive.auditlog.html @@ -8,7 +8,7 @@
{{ user.username }} - It's you! + {{ 'usergroup.you' | translate }}
- {{ log.class }} + {{ 'directive.auditlog.' + log.class | translate }} {{ 'directive.auditlog.log_created' | translate }} {{ 'directive.auditlog.log_updated' | translate }} diff --git a/docs-web/src/main/webapp/src/partial/docs/settings.config.html b/docs-web/src/main/webapp/src/partial/docs/settings.config.html index 8b913897..4a4d6214 100644 --- a/docs-web/src/main/webapp/src/partial/docs/settings.config.html +++ b/docs-web/src/main/webapp/src/partial/docs/settings.config.html @@ -1,7 +1,7 @@

- {{ app.guest_login ? 'Enabled' : 'Disabled' }} + {{ app.guest_login ? 'enabled' : 'disabled' | translate }}

diff --git a/docs-web/src/main/webapp/src/partial/docs/settings.vocabulary.html b/docs-web/src/main/webapp/src/partial/docs/settings.vocabulary.html index 7d6c871b..9bc60aa1 100644 --- a/docs-web/src/main/webapp/src/partial/docs/settings.vocabulary.html +++ b/docs-web/src/main/webapp/src/partial/docs/settings.vocabulary.html @@ -23,7 +23,7 @@

- + From 3217c67ff6d915dc21c05174b4182a89a5c2683d Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Thu, 2 Nov 2017 21:00:32 +0100 Subject: [PATCH 027/288] flat design --- .../src/partial/docs/settings.security.html | 2 +- .../src/main/webapp/src/style/bootstrap.css | 99 ------------------- docs-web/src/main/webapp/src/style/main.less | 3 +- 3 files changed, 2 insertions(+), 102 deletions(-) diff --git a/docs-web/src/main/webapp/src/partial/docs/settings.security.html b/docs-web/src/main/webapp/src/partial/docs/settings.security.html index 33ed422d..94ab86e3 100644 --- a/docs-web/src/main/webapp/src/partial/docs/settings.security.html +++ b/docs-web/src/main/webapp/src/partial/docs/settings.security.html @@ -1,7 +1,7 @@

- {{ user.totp_enabled ? 'Enabled' : 'Disabled' }} + {{ user.totp_enabled ? 'enabled' : 'disabled' | translate }}

diff --git a/docs-web/src/main/webapp/src/style/bootstrap.css b/docs-web/src/main/webapp/src/style/bootstrap.css index 3d6b3daa..967a5e4f 100644 --- a/docs-web/src/main/webapp/src/style/bootstrap.css +++ b/docs-web/src/main/webapp/src/style/bootstrap.css @@ -1125,9 +1125,6 @@ img { max-width: 100%; height: auto; } -.img-rounded { - border-radius: 6px; -} .img-thumbnail { display: inline-block; max-width: 100%; @@ -1136,7 +1133,6 @@ img { line-height: 1.42857143; background-color: #fff; border: 1px solid #ddd; - border-radius: 4px; -webkit-transition: all .2s ease-in-out; -o-transition: all .2s ease-in-out; transition: all .2s ease-in-out; @@ -1539,16 +1535,12 @@ code { font-size: 90%; color: #c7254e; background-color: #f9f2f4; - border-radius: 4px; } kbd { padding: 2px 4px; font-size: 90%; color: #fff; background-color: #333; - border-radius: 3px; - -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .25); - box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .25); } kbd kbd { padding: 0; @@ -1568,7 +1560,6 @@ pre { word-wrap: break-word; background-color: #f5f5f5; border: 1px solid #ccc; - border-radius: 4px; } pre code { padding: 0; @@ -1576,7 +1567,6 @@ pre code { color: inherit; white-space: pre-wrap; background-color: transparent; - border-radius: 0; } .pre-scrollable { max-height: 340px; @@ -2559,7 +2549,6 @@ output { background-color: #fff; background-image: none; border: 1px solid #ccc; - border-radius: 4px; -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); -webkit-transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s; @@ -2569,8 +2558,6 @@ output { .form-control:focus { border-color: #66afe9; outline: 0; - -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, .6); - box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, .6); } .form-control::-moz-placeholder { color: #999; @@ -2711,7 +2698,6 @@ fieldset[disabled] .checkbox label { padding: 5px 10px; font-size: 12px; line-height: 1.5; - border-radius: 3px; } select.input-sm { height: 30px; @@ -2726,7 +2712,6 @@ select[multiple].input-sm { padding: 5px 10px; font-size: 12px; line-height: 1.5; - border-radius: 3px; } .form-group-sm select.form-control { height: 30px; @@ -2748,7 +2733,6 @@ select[multiple].input-sm { padding: 10px 16px; font-size: 18px; line-height: 1.3333333; - border-radius: 6px; } select.input-lg { height: 46px; @@ -2763,7 +2747,6 @@ select[multiple].input-lg { padding: 10px 16px; font-size: 18px; line-height: 1.3333333; - border-radius: 6px; } .form-group-lg select.form-control { height: 46px; @@ -3021,7 +3004,6 @@ select[multiple].input-lg { user-select: none; background-image: none; border: 1px solid transparent; - border-radius: 4px; } .btn:focus, .btn:active:focus, @@ -3404,7 +3386,6 @@ fieldset[disabled] .btn-danger.focus { .btn-link { font-weight: normal; color: #337ab7; - border-radius: 0; } .btn-link, .btn-link:active, @@ -3439,21 +3420,18 @@ fieldset[disabled] .btn-link:focus { padding: 10px 16px; font-size: 18px; line-height: 1.3333333; - border-radius: 6px; } .btn-sm, .btn-group-sm > .btn { padding: 5px 10px; font-size: 12px; line-height: 1.5; - border-radius: 3px; } .btn-xs, .btn-group-xs > .btn { padding: 1px 5px; font-size: 12px; line-height: 1.5; - border-radius: 3px; } .btn-block { display: block; @@ -3538,7 +3516,6 @@ tbody.collapse.in { background-clip: padding-box; border: 1px solid #ccc; border: 1px solid rgba(0, 0, 0, .15); - border-radius: 4px; -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, .175); box-shadow: 0 6px 12px rgba(0, 0, 0, .175); } @@ -3685,9 +3662,6 @@ tbody.collapse.in { .btn-toolbar > .input-group { margin-left: 5px; } -.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) { - border-radius: 0; -} .btn-group > .btn:first-child { margin-left: 0; } @@ -3703,9 +3677,6 @@ tbody.collapse.in { .btn-group > .btn-group { float: left; } -.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn { - border-radius: 0; -} .btn-group > .btn-group:first-child:not(:last-child) > .btn:last-child, .btn-group > .btn-group:first-child:not(:last-child) > .dropdown-toggle { border-top-right-radius: 0; @@ -3763,9 +3734,6 @@ tbody.collapse.in { margin-top: -1px; margin-left: 0; } -.btn-group-vertical > .btn:not(:first-child):not(:last-child) { - border-radius: 0; -} .btn-group-vertical > .btn:first-child:not(:last-child) { border-top-left-radius: 4px; border-top-right-radius: 4px; @@ -3778,9 +3746,6 @@ tbody.collapse.in { border-bottom-right-radius: 4px; border-bottom-left-radius: 4px; } -.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn { - border-radius: 0; -} .btn-group-vertical > .btn-group:first-child:not(:last-child) > .btn:last-child, .btn-group-vertical > .btn-group:first-child:not(:last-child) > .dropdown-toggle { border-bottom-right-radius: 0; @@ -3843,7 +3808,6 @@ tbody.collapse.in { padding: 10px 16px; font-size: 18px; line-height: 1.3333333; - border-radius: 6px; } select.input-group-lg > .form-control, select.input-group-lg > .input-group-addon, @@ -3866,7 +3830,6 @@ select[multiple].input-group-lg > .input-group-btn > .btn { padding: 5px 10px; font-size: 12px; line-height: 1.5; - border-radius: 3px; } select.input-group-sm > .form-control, select.input-group-sm > .input-group-addon, @@ -3887,11 +3850,6 @@ select[multiple].input-group-sm > .input-group-btn > .btn { .input-group .form-control { display: table-cell; } -.input-group-addon:not(:first-child):not(:last-child), -.input-group-btn:not(:first-child):not(:last-child), -.input-group .form-control:not(:first-child):not(:last-child) { - border-radius: 0; -} .input-group-addon, .input-group-btn { width: 1%; @@ -3907,17 +3865,14 @@ select[multiple].input-group-sm > .input-group-btn > .btn { text-align: center; background-color: #eee; border: 1px solid #ccc; - border-radius: 4px; } .input-group-addon.input-sm { padding: 5px 10px; font-size: 12px; - border-radius: 3px; } .input-group-addon.input-lg { padding: 10px 16px; font-size: 18px; - border-radius: 6px; } .input-group-addon input[type="radio"], .input-group-addon input[type="checkbox"] { @@ -4029,7 +3984,6 @@ select[multiple].input-group-sm > .input-group-btn > .btn { margin-right: 2px; line-height: 1.42857143; border: 1px solid transparent; - border-radius: 4px 4px 0 0; } .nav-tabs > li > a:hover { border-color: #eee #eee #ddd; @@ -4069,7 +4023,6 @@ select[multiple].input-group-sm > .input-group-btn > .btn { } .nav-tabs.nav-justified > li > a { margin-right: 0; - border-radius: 4px; } .nav-tabs.nav-justified > .active > a, .nav-tabs.nav-justified > .active > a:hover, @@ -4079,7 +4032,6 @@ select[multiple].input-group-sm > .input-group-btn > .btn { @media (min-width: 768px) { .nav-tabs.nav-justified > li > a { border-bottom: 1px solid #ddd; - border-radius: 4px 4px 0 0; } .nav-tabs.nav-justified > .active > a, .nav-tabs.nav-justified > .active > a:hover, @@ -4090,9 +4042,6 @@ select[multiple].input-group-sm > .input-group-btn > .btn { .nav-pills > li { float: left; } -.nav-pills > li > a { - border-radius: 4px; -} .nav-pills > li + li { margin-left: 2px; } @@ -4137,7 +4086,6 @@ select[multiple].input-group-sm > .input-group-btn > .btn { } .nav-tabs-justified > li > a { margin-right: 0; - border-radius: 4px; } .nav-tabs-justified > .active > a, .nav-tabs-justified > .active > a:hover, @@ -4147,7 +4095,6 @@ select[multiple].input-group-sm > .input-group-btn > .btn { @media (min-width: 768px) { .nav-tabs-justified > li > a { border-bottom: 1px solid #ddd; - border-radius: 4px 4px 0 0; } .nav-tabs-justified > .active > a, .nav-tabs-justified > .active > a:hover, @@ -4172,11 +4119,6 @@ select[multiple].input-group-sm > .input-group-btn > .btn { margin-bottom: 20px; border: 1px solid transparent; } -@media (min-width: 768px) { - .navbar { - border-radius: 4px; - } -} @media (min-width: 768px) { .navbar-header { float: left; @@ -4247,11 +4189,6 @@ select[multiple].input-group-sm > .input-group-btn > .btn { z-index: 1000; border-width: 0 0 1px; } -@media (min-width: 768px) { - .navbar-static-top { - border-radius: 0; - } -} .navbar-fixed-top, .navbar-fixed-bottom { position: fixed; @@ -4259,12 +4196,6 @@ select[multiple].input-group-sm > .input-group-btn > .btn { left: 0; z-index: 1030; } -@media (min-width: 768px) { - .navbar-fixed-top, - .navbar-fixed-bottom { - border-radius: 0; - } -} .navbar-fixed-top { top: 0; border-width: 0 0 1px; @@ -4304,7 +4235,6 @@ select[multiple].input-group-sm > .input-group-btn > .btn { background-color: transparent; background-image: none; border: 1px solid transparent; - border-radius: 4px; } .navbar-toggle:focus { outline: 0; @@ -4313,7 +4243,6 @@ select[multiple].input-group-sm > .input-group-btn > .btn { display: block; width: 22px; height: 2px; - border-radius: 1px; } .navbar-toggle .icon-bar + .icon-bar { margin-top: 4px; @@ -4593,7 +4522,6 @@ fieldset[disabled] .navbar-default .btn-link:focus { } .navbar-inverse { background-color: #222; - border-color: #080808; } .navbar-inverse .navbar-brand { color: #9d9d9d; @@ -4698,7 +4626,6 @@ fieldset[disabled] .navbar-inverse .btn-link:focus { margin-bottom: 20px; list-style: none; background-color: #f5f5f5; - border-radius: 4px; } .breadcrumb > li { display: inline-block; @@ -4715,7 +4642,6 @@ fieldset[disabled] .navbar-inverse .btn-link:focus { display: inline-block; padding-left: 0; margin: 20px 0; - border-radius: 4px; } .pagination > li { display: inline; @@ -4822,7 +4748,6 @@ fieldset[disabled] .navbar-inverse .btn-link:focus { padding: 5px 14px; background-color: #fff; border: 1px solid #ddd; - border-radius: 15px; } .pager li > a:hover, .pager li > a:focus { @@ -4855,7 +4780,6 @@ fieldset[disabled] .navbar-inverse .btn-link:focus { text-align: center; white-space: nowrap; vertical-align: baseline; - border-radius: .25em; } a.label:hover, a.label:focus { @@ -4924,7 +4848,6 @@ a.label:focus { white-space: nowrap; vertical-align: middle; background-color: #777; - border-radius: 10px; } .badge:empty { display: none; @@ -4981,7 +4904,6 @@ a.badge:focus { .container-fluid .jumbotron { padding-right: 15px; padding-left: 15px; - border-radius: 6px; } .jumbotron .container { max-width: 100%; @@ -5008,7 +4930,6 @@ a.badge:focus { line-height: 1.42857143; background-color: #fff; border: 1px solid #ddd; - border-radius: 4px; -webkit-transition: border .2s ease-in-out; -o-transition: border .2s ease-in-out; transition: border .2s ease-in-out; @@ -5031,7 +4952,6 @@ a.thumbnail.active { padding: 15px; margin-bottom: 20px; border: 1px solid transparent; - border-radius: 4px; } .alert h4 { margin-top: 0; @@ -5131,9 +5051,6 @@ a.thumbnail.active { margin-bottom: 20px; overflow: hidden; background-color: #f5f5f5; - border-radius: 4px; - -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, .1); - box-shadow: inset 0 1px 2px rgba(0, 0, 0, .1); } .progress-bar { float: left; @@ -5144,8 +5061,6 @@ a.thumbnail.active { color: #fff; text-align: center; background-color: #337ab7; - -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .15); - box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .15); -webkit-transition: width .6s ease; -o-transition: width .6s ease; transition: width .6s ease; @@ -5454,7 +5369,6 @@ button.list-group-item-danger.active:focus { margin-bottom: 20px; background-color: #fff; border: 1px solid transparent; - border-radius: 4px; -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, .05); box-shadow: 0 1px 1px rgba(0, 0, 0, .05); } @@ -5497,7 +5411,6 @@ button.list-group-item-danger.active:focus { .panel > .list-group .list-group-item, .panel > .panel-collapse > .list-group .list-group-item { border-width: 1px 0; - border-radius: 0; } .panel > .list-group:first-child .list-group-item:first-child, .panel > .panel-collapse > .list-group:first-child .list-group-item:first-child { @@ -5667,7 +5580,6 @@ button.list-group-item-danger.active:focus { } .panel-group .panel { margin-bottom: 0; - border-radius: 4px; } .panel-group .panel + .panel { margin-top: 5px; @@ -5824,10 +5736,6 @@ button.list-group-item-danger.active:focus { padding: 19px; margin-bottom: 20px; background-color: #f5f5f5; - border: 1px solid #e3e3e3; - border-radius: 4px; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05); } .well blockquote { border-color: #ddd; @@ -5835,11 +5743,9 @@ button.list-group-item-danger.active:focus { } .well-lg { padding: 24px; - border-radius: 6px; } .well-sm { padding: 9px; - border-radius: 3px; } .close { float: right; @@ -5912,7 +5818,6 @@ button.close { background-clip: padding-box; border: 1px solid #999; border: 1px solid rgba(0, 0, 0, .2); - border-radius: 6px; outline: 0; -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, .5); box-shadow: 0 3px 9px rgba(0, 0, 0, .5); @@ -6039,7 +5944,6 @@ button.close { color: #fff; text-align: center; background-color: #000; - border-radius: 4px; } .tooltip-arrow { position: absolute; @@ -6132,7 +6036,6 @@ button.close { background-clip: padding-box; border: 1px solid #ccc; border: 1px solid rgba(0, 0, 0, .2); - border-radius: 6px; -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, .2); box-shadow: 0 5px 10px rgba(0, 0, 0, .2); @@ -6156,7 +6059,6 @@ button.close { font-size: 14px; background-color: #f7f7f7; border-bottom: 1px solid #ebebeb; - border-radius: 5px 5px 0 0; } .popover-content { padding: 9px 14px; @@ -6411,7 +6313,6 @@ button.close { background-color: #000 \9; background-color: rgba(0, 0, 0, 0); border: 1px solid #fff; - border-radius: 10px; } .carousel-indicators .active { width: 12px; diff --git a/docs-web/src/main/webapp/src/style/main.less b/docs-web/src/main/webapp/src/style/main.less index 95c27a17..652397f6 100644 --- a/docs-web/src/main/webapp/src/style/main.less +++ b/docs-web/src/main/webapp/src/style/main.less @@ -268,7 +268,6 @@ input[readonly].share-link { .login-box { background: rgba(255, 255, 255, 0.5); padding: 20px; - border-radius: 4px; .help-block, .checkbox { color: white; @@ -309,4 +308,4 @@ input[readonly].share-link { left: 0; right: 0; z-index: 99999; -} \ No newline at end of file +} From 1856ccc3aa92c7f0473023f75e229acd3d96c236 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Thu, 2 Nov 2017 21:16:47 +0100 Subject: [PATCH 028/288] less padding --- .../src/main/webapp/src/partial/docs/directive.acledit.html | 4 ++-- docs-web/src/main/webapp/src/style/bootstrap.css | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docs-web/src/main/webapp/src/partial/docs/directive.acledit.html b/docs-web/src/main/webapp/src/partial/docs/directive.acledit.html index 83cb6051..11c27e81 100644 --- a/docs-web/src/main/webapp/src/partial/docs/directive.acledit.html +++ b/docs-web/src/main/webapp/src/partial/docs/directive.acledit.html @@ -24,7 +24,7 @@
-
+
-
+
+
+ + {{ 'document.edit.additional_metadata' | translate }} + + +
+
+ +
+ +
-
-
- -
- +
+ +
+ +
-
-
- -
- +
+ +
+ +
-
-
- -
- +
+ +
+ +
-
-
- -
- +
+ +
+ +
-
-
- -
- +
+ +
+ +
-
-
- -
- +
+ +
+ +
-
-
- -
- +
+ +
+ +
-
-
- -
- +
+ +
+ +
- +
From a980930e6907cccd48092300e4e07010d4ad9cc6 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Thu, 2 Nov 2017 23:36:38 +0100 Subject: [PATCH 030/288] Closes #137: upload files without drag & drop --- docs-web/src/main/webapp/src/locale/en.json | 4 +++- docs-web/src/main/webapp/src/locale/fr.json | 4 +++- .../webapp/src/partial/docs/document.default.html | 11 +++++++++++ .../webapp/src/partial/docs/document.edit.html | 14 +++++++------- .../src/partial/docs/document.view.content.html | 10 ++++++++++ 5 files changed, 34 insertions(+), 9 deletions(-) diff --git a/docs-web/src/main/webapp/src/locale/en.json b/docs-web/src/main/webapp/src/locale/en.json index b3c356b8..fe6ecf93 100644 --- a/docs-web/src/main/webapp/src/locale/en.json +++ b/docs-web/src/main/webapp/src/locale/en.json @@ -74,7 +74,8 @@ "upload_progress": "Uploading...", "upload_error": "Upload error", "upload_error_quota": "Quota reached", - "drop_zone": "Drag & drop files here to upload" + "drop_zone": "Drag & drop files here to upload", + "add_files": "Add files" }, "permissions": { "permissions": "Permissions", @@ -114,6 +115,7 @@ "upload_error_quota": "Quota reached", "quick_upload": "Quick upload", "drop_zone": "Drag & drop files here to upload", + "add_files": "Add files", "add_new_document": "Add to new document", "latest_activity": "Latest activity", "footer_sismics": "Crafted with by Sismics", diff --git a/docs-web/src/main/webapp/src/locale/fr.json b/docs-web/src/main/webapp/src/locale/fr.json index a07c9a90..c508ff80 100644 --- a/docs-web/src/main/webapp/src/locale/fr.json +++ b/docs-web/src/main/webapp/src/locale/fr.json @@ -74,7 +74,8 @@ "upload_progress": "Envoi...", "upload_error": "Erreur d'envoi", "upload_error_quota": "Quota atteint", - "drop_zone": "Glisser & déposer des fichiers ici pour les envoyer" + "drop_zone": "Glisser & déposer des fichiers ici pour les envoyer", + "add_files": "Ajouter des fichiers" }, "permissions": { "permissions": "Permissions", @@ -114,6 +115,7 @@ "upload_error_quota": "Quota atteint", "quick_upload": "Envoi rapide", "drop_zone": "Glisser & déposer des fichiers ici pour les envoyer", + "add_files": "Ajouter des fichiers", "add_new_document": "Ajouter à un nouveau document", "latest_activity": "Activité récente", "footer_sismics": "Conçu avec par Sismics", diff --git a/docs-web/src/main/webapp/src/partial/docs/document.default.html b/docs-web/src/main/webapp/src/partial/docs/document.default.html index 5b12dc1b..3d7be06f 100644 --- a/docs-web/src/main/webapp/src/partial/docs/document.default.html +++ b/docs-web/src/main/webapp/src/partial/docs/document.default.html @@ -10,6 +10,7 @@ +
@@ -35,6 +36,16 @@ {{ 'document.default.drop_zone' | translate }}

+ +
+
+ +
diff --git a/docs-web/src/main/webapp/src/partial/docs/document.edit.html b/docs-web/src/main/webapp/src/partial/docs/document.edit.html index b021ec2d..d60a298a 100644 --- a/docs-web/src/main/webapp/src/partial/docs/document.edit.html +++ b/docs-web/src/main/webapp/src/partial/docs/document.edit.html @@ -1,6 +1,13 @@
+
+

{{ 'document.edit.uploading_files' | translate }}

+
+
+ + {{ alert.msg }} +
- -
-

{{ 'document.edit.uploading_files' | translate }}

-
-
- - {{ alert.msg }}
\ No newline at end of file diff --git a/docs-web/src/main/webapp/src/partial/docs/document.view.content.html b/docs-web/src/main/webapp/src/partial/docs/document.view.content.html index cfd09e11..c7afc320 100644 --- a/docs-web/src/main/webapp/src/partial/docs/document.view.content.html +++ b/docs-web/src/main/webapp/src/partial/docs/document.view.content.html @@ -72,6 +72,16 @@ {{ 'document.view.content.drop_zone' | translate }}

+ +
+
+ +
From 14b4e5aeec1792b58d093a3a732968bcbc22b26a Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Fri, 3 Nov 2017 00:10:17 +0100 Subject: [PATCH 031/288] design refresh --- .../docs/rest/resource/ThemeResource.java | 4 +-- docs-web/src/main/webapp/src/index.html | 2 +- .../webapp/src/partial/docs/document.html | 2 +- .../src/partial/docs/settings.group.html | 2 +- .../webapp/src/partial/docs/settings.log.html | 2 +- .../src/partial/docs/settings.session.html | 2 +- .../src/partial/docs/settings.user.html | 2 +- .../src/partial/docs/settings.vocabulary.html | 2 +- .../src/main/webapp/src/partial/docs/tag.html | 2 +- .../webapp/src/partial/docs/usergroup.html | 4 +-- .../src/main/webapp/src/style/bootstrap.css | 27 ++++++++++--------- docs-web/src/main/webapp/src/style/main.less | 3 ++- .../sismics/docs/rest/TestThemeResource.java | 4 +-- 13 files changed, 30 insertions(+), 28 deletions(-) diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/ThemeResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/ThemeResource.java index dbd7bd15..66f2c620 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/ThemeResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/ThemeResource.java @@ -58,7 +58,7 @@ public class ThemeResource extends BaseResource { // Build the stylesheet StringBuilder sb = new StringBuilder(); sb.append(new Selector(".navbar") - .rule("background-color", themeConfig.getString("color", "#263238"))); + .rule("background-color", themeConfig.getString("color", "#24292e"))); sb.append(themeConfig.getString("css", "")); return Response.ok().entity(sb.toString()).build(); @@ -83,7 +83,7 @@ public class ThemeResource extends BaseResource { JsonObject themeConfig = getThemeConfig(); JsonObjectBuilder json = Json.createObjectBuilder(); json.add("name", themeConfig.getString("name", "Sismics Docs")); - json.add("color", themeConfig.getString("color", "#263238")); + json.add("color", themeConfig.getString("color", "#24292e")); json.add("css", themeConfig.getString("css", "")); return Response.ok().entity(json.build()).build(); } diff --git a/docs-web/src/main/webapp/src/index.html b/docs-web/src/main/webapp/src/index.html index 85dc910a..1c4cc18a 100644 --- a/docs-web/src/main/webapp/src/index.html +++ b/docs-web/src/main/webapp/src/index.html @@ -114,7 +114,7 @@
  • {{ 'index.nav_tags' | translate }}
  • -
  • +
  • {{ 'index.nav_users_groups' | translate }}
  • diff --git a/docs-web/src/main/webapp/src/partial/docs/document.html b/docs-web/src/main/webapp/src/partial/docs/document.html index a02c7c0a..f102a0e2 100644 --- a/docs-web/src/main/webapp/src/partial/docs/document.html +++ b/docs-web/src/main/webapp/src/partial/docs/document.html @@ -37,7 +37,7 @@
    - +
    diff --git a/docs-web/src/main/webapp/src/partial/docs/settings.group.html b/docs-web/src/main/webapp/src/partial/docs/settings.group.html index 89d85927..8d324be2 100644 --- a/docs-web/src/main/webapp/src/partial/docs/settings.group.html +++ b/docs-web/src/main/webapp/src/partial/docs/settings.group.html @@ -5,7 +5,7 @@
    -
    {{ 'document.title' | translate }}
    +
    diff --git a/docs-web/src/main/webapp/src/partial/docs/settings.log.html b/docs-web/src/main/webapp/src/partial/docs/settings.log.html index f6843bed..a417918f 100644 --- a/docs-web/src/main/webapp/src/partial/docs/settings.log.html +++ b/docs-web/src/main/webapp/src/partial/docs/settings.log.html @@ -1,5 +1,5 @@

    -
    {{ 'settings.group.name' | translate }}
    +
    diff --git a/docs-web/src/main/webapp/src/partial/docs/settings.session.html b/docs-web/src/main/webapp/src/partial/docs/settings.session.html index e7da00d7..9790506d 100644 --- a/docs-web/src/main/webapp/src/partial/docs/settings.session.html +++ b/docs-web/src/main/webapp/src/partial/docs/settings.session.html @@ -1,5 +1,5 @@

    -
    {{ 'settings.log.date' | translate }}
    +
    diff --git a/docs-web/src/main/webapp/src/partial/docs/settings.user.html b/docs-web/src/main/webapp/src/partial/docs/settings.user.html index e0a9fb64..7a90def5 100644 --- a/docs-web/src/main/webapp/src/partial/docs/settings.user.html +++ b/docs-web/src/main/webapp/src/partial/docs/settings.user.html @@ -5,7 +5,7 @@
    -
    {{ 'settings.session.created_date' | translate }}
    +
    diff --git a/docs-web/src/main/webapp/src/partial/docs/settings.vocabulary.html b/docs-web/src/main/webapp/src/partial/docs/settings.vocabulary.html index 9bc60aa1..17f0e6a3 100644 --- a/docs-web/src/main/webapp/src/partial/docs/settings.vocabulary.html +++ b/docs-web/src/main/webapp/src/partial/docs/settings.vocabulary.html @@ -12,7 +12,7 @@ -
    {{ 'settings.user.username' | translate }}
    +
    diff --git a/docs-web/src/main/webapp/src/partial/docs/tag.html b/docs-web/src/main/webapp/src/partial/docs/tag.html index 17c2f6b3..f6968136 100644 --- a/docs-web/src/main/webapp/src/partial/docs/tag.html +++ b/docs-web/src/main/webapp/src/partial/docs/tag.html @@ -16,7 +16,7 @@

    -
    {{ 'settings.vocabulary.value' | translate }}
    +
    diff --git a/docs-web/src/main/webapp/src/partial/docs/usergroup.html b/docs-web/src/main/webapp/src/partial/docs/usergroup.html index 1f169664..7d0865f6 100644 --- a/docs-web/src/main/webapp/src/partial/docs/usergroup.html +++ b/docs-web/src/main/webapp/src/partial/docs/usergroup.html @@ -6,7 +6,7 @@

    -
    +
    @@ -24,7 +24,7 @@

    -
    +
    diff --git a/docs-web/src/main/webapp/src/style/bootstrap.css b/docs-web/src/main/webapp/src/style/bootstrap.css index f70c302e..9197e2c0 100644 --- a/docs-web/src/main/webapp/src/style/bootstrap.css +++ b/docs-web/src/main/webapp/src/style/bootstrap.css @@ -1082,7 +1082,7 @@ html { -webkit-tap-highlight-color: rgba(0, 0, 0, 0); } body { - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-size: 14px; line-height: 1.42857143; color: #333; @@ -1181,7 +1181,7 @@ h6, .h5, .h6 { font-family: inherit; - font-weight: 500; + font-weight: normal; line-height: 1.1; color: inherit; } @@ -1209,9 +1209,9 @@ h6 .small, .h4 .small, .h5 .small, .h6 .small { - font-weight: normal; + font-weight: 300; line-height: 1; - color: #777; + color: #a3aab1; } h1, .h1, @@ -2558,6 +2558,7 @@ output { .form-control:focus { border-color: #66afe9; outline: 0; + box-shadow: inset 0 1px 2px rgba(27,31,35,0.075), 0 0 0 0.2em rgba(3,102,214,0.3); } .form-control::-moz-placeholder { color: #999; @@ -2874,8 +2875,7 @@ select[multiple].input-lg { } .has-error .form-control:focus { border-color: #843534; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #ce8483; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #ce8483; + box-shadow: inset 0 1px 2px rgba(27,31,35,0.075), 0 0 0 0.2em #ce8483; } .has-error .input-group-addon { color: #a94442; @@ -3946,7 +3946,6 @@ select[multiple].input-group-sm > .input-group-btn > .btn { .nav > li > a:hover, .nav > li > a:focus { text-decoration: none; - background-color: #eee; } .nav > li.disabled > a { color: #777; @@ -3984,18 +3983,20 @@ select[multiple].input-group-sm > .input-group-btn > .btn { margin-right: 2px; line-height: 1.42857143; border: 1px solid transparent; + color: #586069; } .nav-tabs > li > a:hover { - border-color: #eee #eee #ddd; + color: #24292e; } .nav-tabs > li.active > a, .nav-tabs > li.active > a:hover, .nav-tabs > li.active > a:focus { - color: #555; + color: #24292e; cursor: default; background-color: #fff; border: 1px solid #ddd; border-bottom-color: transparent; + border-top: 3px solid #e36209; } .nav-tabs.nav-justified { width: 100%; @@ -4535,7 +4536,8 @@ fieldset[disabled] .navbar-default .btn-link:focus { color: #9d9d9d; } .navbar-inverse .navbar-nav > li > a { - color: #9d9d9d; + color: rgba(255,255,255,0.75); + font-weight: bold; } .navbar-inverse .navbar-nav > li > a:hover, .navbar-inverse .navbar-nav > li > a:focus { @@ -4546,7 +4548,6 @@ fieldset[disabled] .navbar-default .btn-link:focus { .navbar-inverse .navbar-nav > .active > a:hover, .navbar-inverse .navbar-nav > .active > a:focus { color: #fff; - background-color: #080808; } .navbar-inverse .navbar-nav > .disabled > a, .navbar-inverse .navbar-nav > .disabled > a:hover, @@ -5897,7 +5898,7 @@ button.close { position: absolute; z-index: 1070; display: block; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-family: inherit; font-size: 12px; font-style: normal; font-weight: normal; @@ -6015,7 +6016,7 @@ button.close { display: none; max-width: 276px; padding: 1px; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-family: inherit; font-size: 14px; font-style: normal; font-weight: normal; diff --git a/docs-web/src/main/webapp/src/style/main.less b/docs-web/src/main/webapp/src/style/main.less index 652397f6..4a8c7a7e 100644 --- a/docs-web/src/main/webapp/src/style/main.less +++ b/docs-web/src/main/webapp/src/style/main.less @@ -9,7 +9,7 @@ // Navbar color .navbar { - background-color: #263238; + background-color: #24292e; } // Selected table line @@ -309,3 +309,4 @@ input[readonly].share-link { right: 0; z-index: 99999; } + diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestThemeResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestThemeResource.java index 25e47a14..ba4a5908 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestThemeResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestThemeResource.java @@ -32,13 +32,13 @@ public class TestThemeResource extends BaseJerseyTest { // Get the stylesheet anonymously String stylesheet = target().path("/theme/stylesheet").request() .get(String.class); - Assert.assertTrue(stylesheet.contains("background-color: #263238;")); + Assert.assertTrue(stylesheet.contains("background-color: #24292e;")); // Get the theme configuration anonymously JsonObject json = target().path("/theme").request() .get(JsonObject.class); Assert.assertEquals("Sismics Docs", json.getString("name")); - Assert.assertEquals("#263238", json.getString("color")); + Assert.assertEquals("#24292e", json.getString("color")); Assert.assertEquals("", json.getString("css")); // Update the main color as admin From 18f37ec2a86a5a23a20bed2d8cc14c15c06e7600 Mon Sep 17 00:00:00 2001 From: bgamard Date: Fri, 3 Nov 2017 11:05:04 +0100 Subject: [PATCH 032/288] Closes #131: validate only dirty forms --- .../docs/controller/document/DocumentEdit.js | 3 ++ docs-web/src/main/webapp/src/locale/fr.json | 2 +- .../src/partial/docs/document.edit.html | 4 +- .../src/partial/docs/settings.account.html | 12 +++--- .../src/partial/docs/settings.group.edit.html | 14 ++++--- .../src/partial/docs/settings.user.edit.html | 38 ++++++++++--------- .../src/main/webapp/src/partial/docs/tag.html | 4 +- docs-web/src/main/webapp/src/style/main.less | 3 +- 8 files changed, 43 insertions(+), 37 deletions(-) diff --git a/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentEdit.js b/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentEdit.js index e00eabf5..b667c2bc 100644 --- a/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentEdit.js +++ b/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentEdit.js @@ -54,6 +54,9 @@ angular.module('docs').controller('DocumentEdit', function($rootScope, $scope, $ language: 'fra' }; $scope.newFiles = []; + if ($scope.form) { + $scope.form.$setPristine(); + } }; /** diff --git a/docs-web/src/main/webapp/src/locale/fr.json b/docs-web/src/main/webapp/src/locale/fr.json index c508ff80..0792f629 100644 --- a/docs-web/src/main/webapp/src/locale/fr.json +++ b/docs-web/src/main/webapp/src/locale/fr.json @@ -261,7 +261,7 @@ "account": { "password": "Mot de passe", "password_confirm": "Mot de passe (confirmation)", - "updated": "Compte mis à jout avec succès" + "updated": "Compte mis à jour avec succès" }, "config": { "title_guest_access": "Accès invité", diff --git a/docs-web/src/main/webapp/src/partial/docs/document.edit.html b/docs-web/src/main/webapp/src/partial/docs/document.edit.html index d60a298a..dacd109d 100644 --- a/docs-web/src/main/webapp/src/partial/docs/document.edit.html +++ b/docs-web/src/main/webapp/src/partial/docs/document.edit.html @@ -9,7 +9,7 @@ {{ alert.msg }}
    -
    +
    - - - - - - @@ -100,7 +99,7 @@ -
    +
    diff --git a/docs-web/src/main/webapp/src/partial/docs/document.view.html b/docs-web/src/main/webapp/src/partial/docs/document.view.html index 7e015bde..6af82ba9 100644 --- a/docs-web/src/main/webapp/src/partial/docs/document.view.html +++ b/docs-web/src/main/webapp/src/partial/docs/document.view.html @@ -86,7 +86,7 @@
    - diff --git a/docs-web/src/main/webapp/src/partial/docs/tag.html b/docs-web/src/main/webapp/src/partial/docs/tag.html index 51f95da4..8dc9d243 100644 --- a/docs-web/src/main/webapp/src/partial/docs/tag.html +++ b/docs-web/src/main/webapp/src/partial/docs/tag.html @@ -1,6 +1,6 @@ -
    +
    -
    +

      @@ -35,7 +35,7 @@

    -
    +
    \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/style/main.less b/docs-web/src/main/webapp/src/style/main.less index 93b88f39..0bdfd8a5 100644 --- a/docs-web/src/main/webapp/src/style/main.less +++ b/docs-web/src/main/webapp/src/style/main.less @@ -41,37 +41,51 @@ tbody tr { cursor: pointer; - } - - .cell-tags { - padding: 2px; - - .label { - margin-left: 2px; - - .full { - display: none; - } - } - - &:hover { - .tags { - position: absolute; + td { .label { - .full { - display: inline; - } - - .shorten { - display: none; - } + margin-left: 5px; } } - } } } +@media (min-width: 992px) { + .row-full { + overflow: hidden; + margin-top: -20px !important; + } + + .well-full { + margin-left: -15px; + padding-bottom: 1000px; + margin-bottom: -980px; + height: 100%; + background-color: #f5f5f5; + box-shadow: inset -2px 0 0 #e5e5e5; + } + + .col-full { + margin-top: 20px; + } +} + +// Footer +.footer { + border-top: 1px #e1e4e8 solid; + padding: 20px; + + img { + opacity: 0.1; + } +} + +// Drop zone +.drop-zone { + background: none; + border: 2px dashed #eee; +} + // $http loader .loader { position: relative; @@ -199,6 +213,11 @@ input[readonly].share-link { color: #b94a48 !important; } +// Comments +.page-header-comments { + margin: 14px 0 20px; +} + // Dirty Bootstrap 3 fix, see https://github.com/twbs/bootstrap/issues/6686 .row { margin: 0; padding: 0 } .navbar-nav.navbar-right:last-child { @@ -308,4 +327,24 @@ input[readonly].share-link { left: 0; right: 0; z-index: 99999; +} + +// Heart +.glyphicon-heart { + &:hover { + color: #e74c3c; + animation: pulse 1s linear infinite; + } +} + +@keyframes pulse{ + 0% { + transform:scale(1.1); + } + 50%{ + transform:scale(0.8); + } + 100%{ + transform:scale(1.1); + } } \ No newline at end of file From d2f9fcdda01be2c4face9168a92bf7c8cb1d19e5 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Sat, 4 Nov 2017 20:39:39 +0100 Subject: [PATCH 035/288] zh_CN translation + footer fix --- docs-web/src/main/webapp/src/app/docs/app.js | 5 +- .../webapp/src/app/docs/controller/Footer.js | 26 ++ .../src/app/docs/controller/Navigation.js | 2 +- .../controller/document/DocumentDefault.js | 5 - docs-web/src/main/webapp/src/img/sismics.png | Bin 1287 -> 0 bytes docs-web/src/main/webapp/src/index.html | 30 +- .../main/webapp/src/lib/angular.timeago.js | 2 +- docs-web/src/main/webapp/src/locale/en.json | 15 +- docs-web/src/main/webapp/src/locale/fr.json | 25 +- .../src/main/webapp/src/locale/zh_CN.json | 377 ++++++++++++++++++ .../src/partial/docs/document.default.html | 19 +- .../webapp/src/partial/docs/document.html | 7 +- .../main/webapp/src/partial/docs/login.html | 8 + .../src/partial/docs/settings.account.html | 2 +- docs-web/src/main/webapp/src/style/main.less | 6 + 15 files changed, 483 insertions(+), 46 deletions(-) create mode 100644 docs-web/src/main/webapp/src/app/docs/controller/Footer.js delete mode 100644 docs-web/src/main/webapp/src/img/sismics.png create mode 100644 docs-web/src/main/webapp/src/locale/zh_CN.json diff --git a/docs-web/src/main/webapp/src/app/docs/app.js b/docs-web/src/main/webapp/src/app/docs/app.js index 608a0ab7..e578bc7c 100644 --- a/docs-web/src/main/webapp/src/app/docs/app.js +++ b/docs-web/src/main/webapp/src/app/docs/app.js @@ -345,7 +345,8 @@ angular.module('docs', prefix: 'locale/', suffix: '.json' }) - .registerAvailableLanguageKeys(['en', 'fr'], { + .registerAvailableLanguageKeys(['en', 'fr', 'zh_CN', 'zh_HK', 'zh_TW'], { + 'zh_*': 'zh_CN', 'en_*': 'en', 'fr_*': 'fr', '*': 'en' @@ -354,7 +355,7 @@ angular.module('docs', .fallbackLanguage('en'); // Configuring Timago - timeAgoSettings.overrideLang = $translateProvider.proposedLanguage; + timeAgoSettings.overrideLang = $translateProvider.preferredLanguage(); timeAgoSettings.fullDateAfterSeconds = 60 * 60 * 24 * 30; // 30 days // Configuring $http to act like jQuery.ajax diff --git a/docs-web/src/main/webapp/src/app/docs/controller/Footer.js b/docs-web/src/main/webapp/src/app/docs/controller/Footer.js new file mode 100644 index 00000000..f5a60448 --- /dev/null +++ b/docs-web/src/main/webapp/src/app/docs/controller/Footer.js @@ -0,0 +1,26 @@ +'use strict'; + +/** + * Footer controller. + */ +angular.module('docs').controller('Footer', function($scope, Restangular, $translate, timeAgoSettings) { + // Load app data + Restangular.one('app').get().then(function(data) { + $scope.app = data; + }); + + $scope.currentLang = $translate.use(); + + // Change the current language and save it to local storage + $scope.changeLanguage = function(lang) { + $translate.use(lang); + timeAgoSettings.overrideLang = lang; + localStorage.overrideLang = lang; + $scope.currentLang = lang; + }; + + // Set the current language if an override is saved in local storage + if (!_.isUndefined(localStorage.overrideLang)) { + $scope.changeLanguage(localStorage.overrideLang); + } +}); \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/app/docs/controller/Navigation.js b/docs-web/src/main/webapp/src/app/docs/controller/Navigation.js index adbef67b..20c75b19 100644 --- a/docs-web/src/main/webapp/src/app/docs/controller/Navigation.js +++ b/docs-web/src/main/webapp/src/app/docs/controller/Navigation.js @@ -3,7 +3,7 @@ /** * Navigation controller. */ -angular.module('docs').controller('Navigation', function($scope, $http, $state, $rootScope, User, Restangular) { +angular.module('docs').controller('Navigation', function($scope, $state, $rootScope, User, Restangular) { User.userInfo().then(function(data) { $rootScope.userInfo = data; }); diff --git a/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentDefault.js b/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentDefault.js index e093f02c..ea370ac1 100644 --- a/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentDefault.js +++ b/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentDefault.js @@ -4,11 +4,6 @@ * Document default controller. */ angular.module('docs').controller('DocumentDefault', function($scope, $rootScope, $state, Restangular, $upload, $translate) { - // Load app data - Restangular.one('app').get().then(function(data) { - $scope.app = data; - }); - // Load user audit log Restangular.one('auditlog').get().then(function(data) { $scope.logs = data.logs; diff --git a/docs-web/src/main/webapp/src/img/sismics.png b/docs-web/src/main/webapp/src/img/sismics.png deleted file mode 100644 index b4b22e20ad6ec935ac6a42a426e0814ae399d4ec..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1287 zcmaJ>ZA=?=9KKSa9RsNg#%U`1dnC9lw0AvPN-q@pas?_}$4ZqZn+bQlKlB3k!rfsD zn+c;+z|Bt+T-0Q4O9qJvjt?#|xak}*E}B@39~Kio3`G;eR5B7V8s{CO%n#N}?*1=% zp6B=W5iuI(!Q@BZwLBiKLdKyU3IzI2i-u%nGv3Mv&&#tjweap$1quz>9wL*QJk9z;k}I z*F)p99ENGWV@!cPW3fbPY%t~H(AE~voF#Pu0cs4$3PYkwX8q{4E~)R0YYYY35N*(p zK1eD_cZ0B`K;UuWjuhUAgGQIr)!_Dez4d^=T{wmln2T_@ykrAG;yBoOP(7N$^^ z!%8EXAJsE`P=X-u%8KevoAicZSw_ZOPTVM|0Ho>vhYG?jTGit4VZQ$Zhv5# z`rW)i+bq8f+j3Lg`A1d-uvX0;J$F(*zSz`PXZzxU54=`BoBJ&WUbyiSa(q5ioZOC)%@d)V}|R{A8fn)S15cM8E0w*T}Pv?|u7T(%xs9`SbKUl`EZ}IuaUXeapQ0 z?&{LU%&G4Z^&1uWtuOQI*D89WR`bAob;+HrW0UVUCgvvAs`ivtwOH`Ly}O6O-(j?D u?bSc>tFs3!XRhvPe;WUJ+>}8A<0d5i + + + @@ -55,10 +58,8 @@ - - @@ -149,9 +150,28 @@
    -
    -
    {{ 'document.title' | translate }} {{ 'document.creation_date' | translate }}
    +
    + {{ 'document.no_documents' | translate }}
    + {{ document.title }} ({{ document.file_count }}) - {{ document.create_date | date: 'yyyy-MM-dd' }}
    - + + +
    + + + angular.module('ngView', ['pascalprecht.translate']) + + .config(function ($translateProvider) { + + $translateProvider.translations('en',{ + 'TRANSLATION_ID': 'Hello there!', + 'WITH_VALUES': 'The following value is dynamic: {{value}}', + }).preferredLanguage('en'); + + }); + + angular.module('ngView').controller('TranslateCtrl', function ($scope) { + $scope.translationId = 'TRANSLATION_ID'; + + $scope.values = { + value: 78 + }; + }); + + + it('should translate', function () { + inject(function ($rootScope, $compile) { + $rootScope.translationId = 'TRANSLATION_ID'; + + element = $compile('')($rootScope); + $rootScope.$digest(); + expect(element.attr('placeholder)).toBe('Hello there!'); + expect(element.attr('title)).toBe('The following value is dynamic: 5'); + }); + }); + + + */ +.directive('translateAttr', translateAttrDirective); +function translateAttrDirective($translate, $rootScope) { + + 'use strict'; + + return { + restrict: 'A', + priority: $translate.directivePriority(), + link: function linkFn(scope, element, attr) { + + var translateAttr, + translateValues, + translateSanitizeStrategy, + previousAttributes = {}; + + // Main update translations function + var updateTranslations = function () { + angular.forEach(translateAttr, function (translationId, attributeName) { + if (!translationId) { + return; + } + previousAttributes[attributeName] = true; + + // if translation id starts with '.' and translateNamespace given, prepend namespace + if (scope.translateNamespace && translationId.charAt(0) === '.') { + translationId = scope.translateNamespace + translationId; + } + $translate(translationId, translateValues, attr.translateInterpolation, undefined, scope.translateLanguage, translateSanitizeStrategy) + .then(function (translation) { + element.attr(attributeName, translation); + }, function (translationId) { + element.attr(attributeName, translationId); + }); + }); + + // Removing unused attributes that were previously used + angular.forEach(previousAttributes, function (flag, attributeName) { + if (!translateAttr[attributeName]) { + element.removeAttr(attributeName); + delete previousAttributes[attributeName]; + } + }); + }; + + // Watch for attribute changes + watchAttribute( + scope, + attr.translateAttr, + function (newValue) { translateAttr = newValue; }, + updateTranslations + ); + // Watch for value changes + watchAttribute( + scope, + attr.translateValues, + function (newValue) { translateValues = newValue; }, + updateTranslations + ); + // Watch for sanitize strategy changes + watchAttribute( + scope, + attr.translateSanitizeStrategy, + function (newValue) { translateSanitizeStrategy = newValue; }, + updateTranslations + ); + + if (attr.translateValues) { + scope.$watch(attr.translateValues, updateTranslations, true); + } + + // Replaced watcher on translateLanguage with event listener + scope.$on('translateLanguageChanged', updateTranslations); + + // Ensures the text will be refreshed after the current language was changed + // w/ $translate.use(...) + var unbind = $rootScope.$on('$translateChangeSuccess', updateTranslations); + + updateTranslations(); + scope.$on('$destroy', unbind); + } + }; +} + +function watchAttribute(scope, attribute, valueCallback, changeCallback) { + 'use strict'; + if (!attribute) { + return; + } + if (attribute.substr(0, 2) === '::') { + attribute = attribute.substr(2); + } else { + scope.$watch(attribute, function(newValue) { + valueCallback(newValue); + changeCallback(); + }, true); + } + valueCallback(scope.$eval(attribute)); +} + +translateAttrDirective.displayName = 'translateAttrDirective'; + angular.module('pascalprecht.translate') /** * @ngdoc directive * @name pascalprecht.translate.directive:translateCloak - * @requires $rootScope * @requires $translate * @restrict A * @@ -2948,34 +3457,33 @@ function translateCloakDirective($translate, $rootScope) { 'use strict'; return { - compile: function (tElement) { - var applyCloak = function () { - tElement.addClass($translate.cloakClassName()); - }, - removeCloak = function () { - tElement.removeClass($translate.cloakClassName()); - }; - $translate.onReady(function () { - removeCloak(); - }); - applyCloak(); + compile : function (tElement) { + var applyCloak = function (element) { + element.addClass($translate.cloakClassName()); + }, + removeCloak = function (element) { + element.removeClass($translate.cloakClassName()); + }; + applyCloak(tElement); return function linkFn(scope, iElement, iAttr) { + //Create bound functions that incorporate the active DOM element. + var iRemoveCloak = removeCloak.bind(this, iElement), iApplyCloak = applyCloak.bind(this, iElement); if (iAttr.translateCloak && iAttr.translateCloak.length) { // Register a watcher for the defined translation allowing a fine tuned cloak iAttr.$observe('translateCloak', function (translationId) { - $translate(translationId).then(removeCloak, applyCloak); + $translate(translationId).then(iRemoveCloak, iApplyCloak); }); - // Register for change events as this is being another indicicator revalidating the cloak) $rootScope.$on('$translateChangeSuccess', function () { - $translate(iAttr.translateCloak).then(removeCloak, applyCloak); + $translate(iAttr.translateCloak).then(iRemoveCloak, iApplyCloak); }); + } else { + $translate.onReady(iRemoveCloak); } }; } }; } -translateCloakDirective.$inject = ['$translate', '$rootScope']; translateCloakDirective.displayName = 'translateCloakDirective'; @@ -3110,7 +3618,7 @@ angular.module('pascalprecht.translate') .translations('de',{ 'HELLO': 'Hallo Welt!' }) - .translations(.preferredLanguage('en'); + .preferredLanguage('en'); }); @@ -3128,9 +3636,14 @@ function translateLanguageDirective() { scope: true, compile: function () { return function linkFn(scope, iElement, iAttrs) { + iAttrs.$observe('translateLanguage', function (newTranslateLanguage) { scope.translateLanguage = newTranslateLanguage; }); + + scope.$watch('translateLanguage', function(){ + scope.$broadcast('translateLanguageChanged'); + }); }; } }; @@ -3138,7 +3651,6 @@ function translateLanguageDirective() { translateLanguageDirective.displayName = 'translateLanguageDirective'; - angular.module('pascalprecht.translate') /** * @ngdoc filter @@ -3198,9 +3710,11 @@ function translateFilterFactory($parse, $translate) { 'use strict'; var translateFilter = function (translationId, interpolateParams, interpolation, forceLanguage) { - if (!angular.isObject(interpolateParams)) { - interpolateParams = $parse(interpolateParams)(this); + var ctx = this || { + '__SCOPE_IS_NOT_AVAILABLE': 'More info at https://github.com/angular/angular.js/commit/8863b9d04c722b278fa93c5d66ad1e578ad6eb1f' + }; + interpolateParams = $parse(interpolateParams)(ctx); } return $translate.instant(translationId, interpolateParams, interpolation, forceLanguage); @@ -3212,7 +3726,6 @@ function translateFilterFactory($parse, $translate) { return translateFilter; } -translateFilterFactory.$inject = ['$parse', '$translate']; translateFilterFactory.displayName = 'translateFilterFactory'; @@ -3238,118 +3751,8 @@ function $translationCache($cacheFactory) { return $cacheFactory('translations'); } -$translationCache.$inject = ['$cacheFactory']; $translationCache.displayName = '$translationCache'; return 'pascalprecht.translate'; })); - - -/*! - * angular-translate - v2.9.0 - 2016-01-24 - * - * Copyright (c) 2016 The angular-translate team, Pascal Precht; Licensed MIT - */ -(function (root, factory) { - if (typeof define === 'function' && define.amd) { - // AMD. Register as an anonymous module unless amdModuleId is set - define([], function () { - return (factory()); - }); - } else if (typeof exports === 'object') { - // Node. Does not work with strict CommonJS, but - // only CommonJS-like environments that support module.exports, - // like Node. - module.exports = factory(); - } else { - factory(); - } -}(this, function () { - - angular.module('pascalprecht.translate') - /** - * @ngdoc object - * @name pascalprecht.translate.$translateStaticFilesLoader - * @requires $q - * @requires $http - * - * @description - * Creates a loading function for a typical static file url pattern: - * "lang-en_US.json", "lang-de_DE.json", etc. Using this builder, - * the response of these urls must be an object of key-value pairs. - * - * @param {object} options Options object, which gets prefix, suffix and key. - */ - .factory('$translateStaticFilesLoader', $translateStaticFilesLoader); - - function $translateStaticFilesLoader($q, $http) { - - 'use strict'; - - return function (options) { - - if (!options || (!angular.isArray(options.files) && (!angular.isString(options.prefix) || !angular.isString(options.suffix)))) { - throw new Error('Couldn\'t load static files, no files and prefix or suffix specified!'); - } - - if (!options.files) { - options.files = [{ - prefix: options.prefix, - suffix: options.suffix - }]; - } - - var load = function (file) { - if (!file || (!angular.isString(file.prefix) || !angular.isString(file.suffix))) { - throw new Error('Couldn\'t load static file, no prefix or suffix specified!'); - } - - return $http(angular.extend({ - url: [ - file.prefix, - options.key, - file.suffix - ].join(''), - method: 'GET', - params: '' - }, options.$http)) - .then(function(result) { - return result.data; - }, function () { - return $q.reject(options.key); - }); - }; - - var promises = [], - length = options.files.length; - - for (var i = 0; i < length; i++) { - promises.push(load({ - prefix: options.files[i].prefix, - key: options.key, - suffix: options.files[i].suffix - })); - } - - return $q.all(promises) - .then(function (data) { - var length = data.length, - mergedData = {}; - - for (var i = 0; i < length; i++) { - for (var key in data[i]) { - mergedData[key] = data[i][key]; - } - } - - return mergedData; - }); - }; - } - $translateStaticFilesLoader.$inject = ['$q', '$http']; - - $translateStaticFilesLoader.displayName = '$translateStaticFilesLoader'; - return 'pascalprecht.translate'; - -})); \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/lib/angular.ui-bootstrap.js b/docs-web/src/main/webapp/src/lib/angular.ui-bootstrap.js index 29a74a6a..474e2d3a 100644 --- a/docs-web/src/main/webapp/src/lib/angular.ui-bootstrap.js +++ b/docs-web/src/main/webapp/src/lib/angular.ui-bootstrap.js @@ -1794,6 +1794,20 @@ angular.module('ui.bootstrap.pagination', []) }); } + if (attrs.nextText) { + attrs.$observe('nextText', function(value) { + nextText = paginationCtrl.getAttributeValue(value, config.nextText, true); + paginationCtrl.render(); + }); + } + + if (attrs.previousText) { + attrs.$observe('previousText', function(value) { + previousText = paginationCtrl.getAttributeValue(value, config.previousText, true); + paginationCtrl.render(); + }); + } + // Create page object used in template function makePage(number, text, isActive, isDisabled) { return { diff --git a/docs-web/src/main/webapp/src/partial/docs/document.default.html b/docs-web/src/main/webapp/src/partial/docs/document.default.html index 984c2925..afc7c067 100644 --- a/docs-web/src/main/webapp/src/partial/docs/document.default.html +++ b/docs-web/src/main/webapp/src/partial/docs/document.default.html @@ -1,6 +1,6 @@ -
    +

    {{ 'document.default.quick_upload' | translate }}

    -

    {{ 'document.default.latest_activity' | translate }}

    - +
    +

    {{ 'document.default.latest_activity' | translate }}

    + +
    \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/style/main.less b/docs-web/src/main/webapp/src/style/main.less index 9e3ce839..97a915f1 100644 --- a/docs-web/src/main/webapp/src/style/main.less +++ b/docs-web/src/main/webapp/src/style/main.less @@ -335,6 +335,11 @@ input[readonly].share-link { z-index: 99999; } +// Translate +.translate-cloak { + display: none; +} + // Heart .glyphicon-heart { &:hover { From 879ab7951d8a553d701ee8f242fe690bba607ffc Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Sun, 5 Nov 2017 17:30:45 +0100 Subject: [PATCH 038/288] fix build --- docs-web/src/main/webapp/src/index.html | 1 - .../angular.translate-loader-static-files.js | 112 ----------------- .../main/webapp/src/lib/angular.translate.js | 113 ++++++++++++++++++ 3 files changed, 113 insertions(+), 113 deletions(-) delete mode 100644 docs-web/src/main/webapp/src/lib/angular.translate-loader-static-files.js diff --git a/docs-web/src/main/webapp/src/index.html b/docs-web/src/main/webapp/src/index.html index 3b0e642d..42c0ae2f 100644 --- a/docs-web/src/main/webapp/src/index.html +++ b/docs-web/src/main/webapp/src/index.html @@ -30,7 +30,6 @@ - diff --git a/docs-web/src/main/webapp/src/lib/angular.translate-loader-static-files.js b/docs-web/src/main/webapp/src/lib/angular.translate-loader-static-files.js deleted file mode 100644 index 2e3afe29..00000000 --- a/docs-web/src/main/webapp/src/lib/angular.translate-loader-static-files.js +++ /dev/null @@ -1,112 +0,0 @@ -/*! - * angular-translate - v2.16.0 - 2017-11-01 - * - * Copyright (c) 2017 The angular-translate team, Pascal Precht; Licensed MIT - */ -(function (root, factory) { - if (typeof define === 'function' && define.amd) { - // AMD. Register as an anonymous module unless amdModuleId is set - define([], function () { - return (factory()); - }); - } else if (typeof module === 'object' && module.exports) { - // Node. Does not work with strict CommonJS, but - // only CommonJS-like environments that support module.exports, - // like Node. - module.exports = factory(); - } else { - factory(); - } -}(this, function () { - -$translateStaticFilesLoader.$inject = ['$q', '$http']; -angular.module('pascalprecht.translate') -/** - * @ngdoc object - * @name pascalprecht.translate.$translateStaticFilesLoader - * @requires $q - * @requires $http - * - * @description - * Creates a loading function for a typical static file url pattern: - * "lang-en_US.json", "lang-de_DE.json", etc. Using this builder, - * the response of these urls must be an object of key-value pairs. - * - * @param {object} options Options object, which gets prefix, suffix, key, and fileMap - */ -.factory('$translateStaticFilesLoader', $translateStaticFilesLoader); - -function $translateStaticFilesLoader($q, $http) { - - 'use strict'; - - return function (options) { - - if (!options || (!angular.isArray(options.files) && (!angular.isString(options.prefix) || !angular.isString(options.suffix)))) { - throw new Error('Couldn\'t load static files, no files and prefix or suffix specified!'); - } - - if (!options.files) { - options.files = [{ - prefix: options.prefix, - suffix: options.suffix - }]; - } - - var load = function (file) { - if (!file || (!angular.isString(file.prefix) || !angular.isString(file.suffix))) { - throw new Error('Couldn\'t load static file, no prefix or suffix specified!'); - } - - var fileUrl = [ - file.prefix, - options.key, - file.suffix - ].join(''); - - if (angular.isObject(options.fileMap) && options.fileMap[fileUrl]) { - fileUrl = options.fileMap[fileUrl]; - } - - return $http(angular.extend({ - url: fileUrl, - method: 'GET' - }, options.$http)) - .then(function(result) { - return result.data; - }, function () { - return $q.reject(options.key); - }); - }; - - var promises = [], - length = options.files.length; - - for (var i = 0; i < length; i++) { - promises.push(load({ - prefix: options.files[i].prefix, - key: options.key, - suffix: options.files[i].suffix - })); - } - - return $q.all(promises) - .then(function (data) { - var length = data.length, - mergedData = {}; - - for (var i = 0; i < length; i++) { - for (var key in data[i]) { - mergedData[key] = data[i][key]; - } - } - - return mergedData; - }); - }; -} - -$translateStaticFilesLoader.displayName = '$translateStaticFilesLoader'; -return 'pascalprecht.translate'; - -})); diff --git a/docs-web/src/main/webapp/src/lib/angular.translate.js b/docs-web/src/main/webapp/src/lib/angular.translate.js index bad0b2f3..d00424ff 100644 --- a/docs-web/src/main/webapp/src/lib/angular.translate.js +++ b/docs-web/src/main/webapp/src/lib/angular.translate.js @@ -3756,3 +3756,116 @@ $translationCache.displayName = '$translationCache'; return 'pascalprecht.translate'; })); + +/*! + * angular-translate - v2.16.0 - 2017-11-01 + * + * Copyright (c) 2017 The angular-translate team, Pascal Precht; Licensed MIT + */ +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module unless amdModuleId is set + define([], function () { + return (factory()); + }); + } else if (typeof module === 'object' && module.exports) { + // Node. Does not work with strict CommonJS, but + // only CommonJS-like environments that support module.exports, + // like Node. + module.exports = factory(); + } else { + factory(); + } +}(this, function () { + + $translateStaticFilesLoader.$inject = ['$q', '$http']; + angular.module('pascalprecht.translate') + /** + * @ngdoc object + * @name pascalprecht.translate.$translateStaticFilesLoader + * @requires $q + * @requires $http + * + * @description + * Creates a loading function for a typical static file url pattern: + * "lang-en_US.json", "lang-de_DE.json", etc. Using this builder, + * the response of these urls must be an object of key-value pairs. + * + * @param {object} options Options object, which gets prefix, suffix, key, and fileMap + */ + .factory('$translateStaticFilesLoader', $translateStaticFilesLoader); + + function $translateStaticFilesLoader($q, $http) { + + 'use strict'; + + return function (options) { + + if (!options || (!angular.isArray(options.files) && (!angular.isString(options.prefix) || !angular.isString(options.suffix)))) { + throw new Error('Couldn\'t load static files, no files and prefix or suffix specified!'); + } + + if (!options.files) { + options.files = [{ + prefix: options.prefix, + suffix: options.suffix + }]; + } + + var load = function (file) { + if (!file || (!angular.isString(file.prefix) || !angular.isString(file.suffix))) { + throw new Error('Couldn\'t load static file, no prefix or suffix specified!'); + } + + var fileUrl = [ + file.prefix, + options.key, + file.suffix + ].join(''); + + if (angular.isObject(options.fileMap) && options.fileMap[fileUrl]) { + fileUrl = options.fileMap[fileUrl]; + } + + return $http(angular.extend({ + url: fileUrl, + method: 'GET' + }, options.$http)) + .then(function(result) { + return result.data; + }, function () { + return $q.reject(options.key); + }); + }; + + var promises = [], + length = options.files.length; + + for (var i = 0; i < length; i++) { + promises.push(load({ + prefix: options.files[i].prefix, + key: options.key, + suffix: options.files[i].suffix + })); + } + + return $q.all(promises) + .then(function (data) { + var length = data.length, + mergedData = {}; + + for (var i = 0; i < length; i++) { + for (var key in data[i]) { + mergedData[key] = data[i][key]; + } + } + + return mergedData; + }); + }; + } + + $translateStaticFilesLoader.displayName = '$translateStaticFilesLoader'; + return 'pascalprecht.translate'; + +})); From 614c8a1d139f04d93321405c4327327dc228cb6c Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Sun, 5 Nov 2017 21:27:54 +0100 Subject: [PATCH 039/288] log to stdout --- docs-web/src/prod/resources/log4j.properties | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs-web/src/prod/resources/log4j.properties b/docs-web/src/prod/resources/log4j.properties index 8bccecbe..130264d3 100644 --- a/docs-web/src/prod/resources/log4j.properties +++ b/docs-web/src/prod/resources/log4j.properties @@ -1,4 +1,7 @@ -log4j.rootCategory=WARN, MEMORY +log4j.rootCategory=WARN, CONSOLE, MEMORY +log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender +log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout +log4j.appender.CONSOLE.layout.ConversionPattern=%d{DATE} %p %l %m %n log4j.appender.MEMORY=com.sismics.util.log4j.MemoryAppender log4j.appender.MEMORY.size=1000 From cf9101d157e3b806dbbcd8cbe66d75988f47289b Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Sun, 5 Nov 2017 22:28:23 +0100 Subject: [PATCH 040/288] Closes #143: Select the default language for new documents from browser language --- .../src/app/docs/controller/document/DocumentEdit.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentEdit.js b/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentEdit.js index f10bdf75..a7ec2e48 100644 --- a/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentEdit.js +++ b/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentEdit.js @@ -48,12 +48,21 @@ angular.module('docs').controller('DocumentEdit', function($rootScope, $scope, $ * Reset the form to add a new document. */ $scope.resetForm = function() { + var languages = { + en: 'eng', + fr: 'fra', + zh_CN: 'chi_sim' + }; + var language = languages[$translate.use()]; + $scope.document = { tags: [], relations: [], - language: 'fra' + language: language ? language : 'eng' }; + $scope.newFiles = []; + if ($scope.form) { $scope.form.$setPristine(); } From 4d161aea072722d48fc9ac4b4b9f3f271dbc69a3 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Mon, 6 Nov 2017 00:48:55 +0100 Subject: [PATCH 041/288] Closes #117: fix templates minification --- docs-web/src/main/webapp/Gruntfile.js | 16 ++++++++-------- .../main/webapp/src/app/docs/directive/File.js | 6 ++---- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/docs-web/src/main/webapp/Gruntfile.js b/docs-web/src/main/webapp/Gruntfile.js index e2ebe98f..9e51bcc7 100644 --- a/docs-web/src/main/webapp/Gruntfile.js +++ b/docs-web/src/main/webapp/Gruntfile.js @@ -72,12 +72,12 @@ module.exports = function(grunt) { options: { append: true, htmlmin: { - collapseBooleanAttributes: true, + collapseBooleanAttributes: false, collapseWhitespace: true, - removeAttributeQuotes: true, + removeAttributeQuotes: false, removeComments: true, - removeEmptyAttributes: true, - removeRedundantAttributes: true, + removeEmptyAttributes: false, + removeRedundantAttributes: false, removeScriptTypeAttributes: true, removeStyleLinkTypeAttributes: true } @@ -90,12 +90,12 @@ module.exports = function(grunt) { options: { append: true, htmlmin: { - collapseBooleanAttributes: true, + collapseBooleanAttributes: false, collapseWhitespace: true, - removeAttributeQuotes: true, + removeAttributeQuotes: false, removeComments: true, - removeEmptyAttributes: true, - removeRedundantAttributes: true, + removeEmptyAttributes: false, + removeRedundantAttributes: false, removeScriptTypeAttributes: true, removeStyleLinkTypeAttributes: true } diff --git a/docs-web/src/main/webapp/src/app/docs/directive/File.js b/docs-web/src/main/webapp/src/app/docs/directive/File.js index 974ff98f..9e0b0be7 100644 --- a/docs-web/src/main/webapp/src/app/docs/directive/File.js +++ b/docs-web/src/main/webapp/src/app/docs/directive/File.js @@ -9,12 +9,10 @@ angular.module('docs').directive('file', function() { template: '', replace: true, require: 'ngModel', - link: function(scope, element, attr, ctrl) { + link: function(scope, element, attrs, ctrl) { element.bind('change', function() { scope.$apply(function() { - console.log('is multiple?', attr.multiple); - console.log('setting file directive value', attr.multiple ? element[0].files : element[0].files[0]); - attr.multiple ? ctrl.$setViewValue(element[0].files) : ctrl.$setViewValue(element[0].files[0]); + attrs.multiple ? ctrl.$setViewValue(element[0].files) : ctrl.$setViewValue(element[0].files[0]); }); }); } From 244ddc7ce2059a672c086b1e09e028fb8a40db9d Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Mon, 6 Nov 2017 16:45:47 +0100 Subject: [PATCH 042/288] Closes #141: Never close full file content in memory --- .../core/event/FileCreatedAsyncEvent.java | 35 +-- .../event/TemporaryFileCleanupAsyncEvent.java | 35 +++ .../async/FileCreatedAsyncListener.java | 17 +- .../TemporaryFileCleanupAsyncListener.java | 38 ++++ .../docs/core/model/context/AppContext.java | 10 +- .../docs/core/util/EncryptionUtil.java | 43 +++- .../com/sismics/docs/core/util/FileUtil.java | 51 ++--- .../com/sismics/docs/core/util/PdfUtil.java | 208 +++++++++--------- .../docs/core/util/TemporaryFileStream.java | 55 ----- .../util/context/ThreadLocalContext.java | 27 +++ .../com/sismics/util/mime/MimeTypeUtil.java | 35 +-- .../sismics/docs/core/util/TestFileUtil.java | 51 +++-- .../com/sismics/util/TestMimeTypeUtil.java | 36 ++- .../docs/rest/resource/DocumentResource.java | 10 +- .../docs/rest/resource/FileResource.java | 51 ++--- docs-web/src/main/webapp/src/locale/fr.json | 2 +- .../sismics/docs/rest/TestFileResource.java | 39 ++-- 17 files changed, 389 insertions(+), 354 deletions(-) create mode 100644 docs-core/src/main/java/com/sismics/docs/core/event/TemporaryFileCleanupAsyncEvent.java create mode 100644 docs-core/src/main/java/com/sismics/docs/core/listener/async/TemporaryFileCleanupAsyncListener.java delete mode 100644 docs-core/src/main/java/com/sismics/docs/core/util/TemporaryFileStream.java diff --git a/docs-core/src/main/java/com/sismics/docs/core/event/FileCreatedAsyncEvent.java b/docs-core/src/main/java/com/sismics/docs/core/event/FileCreatedAsyncEvent.java index c6cfe07f..17b4c460 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/event/FileCreatedAsyncEvent.java +++ b/docs-core/src/main/java/com/sismics/docs/core/event/FileCreatedAsyncEvent.java @@ -1,6 +1,7 @@ package com.sismics.docs.core.event; import java.io.InputStream; +import java.nio.file.Path; import com.google.common.base.MoreObjects; import com.sismics.docs.core.model.jpa.File; @@ -22,16 +23,16 @@ public class FileCreatedAsyncEvent extends UserEvent { private String language; /** - * Unencrypted input stream containing the file. + * Unencrypted original file. */ - private InputStream inputStream; + private Path unencryptedFile; /** - * Unencrypted input stream containing a PDF representation - * of the file. May be null if the PDF conversion is not + * Unencrypted file containing PDF representation + * of the original file. May be null if the PDF conversion is not * necessary or not possible. */ - private InputStream pdfInputStream; + private Path unencryptedPdfFile; public File getFile() { return file; @@ -48,21 +49,23 @@ public class FileCreatedAsyncEvent extends UserEvent { public void setLanguage(String language) { this.language = language; } - - public InputStream getInputStream() { - return inputStream; + + public Path getUnencryptedFile() { + return unencryptedFile; } - public void setInputStream(InputStream inputStream) { - this.inputStream = inputStream; - } - - public InputStream getPdfInputStream() { - return pdfInputStream; + public FileCreatedAsyncEvent setUnencryptedFile(Path unencryptedFile) { + this.unencryptedFile = unencryptedFile; + return this; } - public void setPdfInputStream(InputStream pdfInputStream) { - this.pdfInputStream = pdfInputStream; + public Path getUnencryptedPdfFile() { + return unencryptedPdfFile; + } + + public FileCreatedAsyncEvent setUnencryptedPdfFile(Path unencryptedPdfFile) { + this.unencryptedPdfFile = unencryptedPdfFile; + return this; } @Override diff --git a/docs-core/src/main/java/com/sismics/docs/core/event/TemporaryFileCleanupAsyncEvent.java b/docs-core/src/main/java/com/sismics/docs/core/event/TemporaryFileCleanupAsyncEvent.java new file mode 100644 index 00000000..6ee1c6a6 --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/event/TemporaryFileCleanupAsyncEvent.java @@ -0,0 +1,35 @@ +package com.sismics.docs.core.event; + +import com.google.common.base.MoreObjects; +import com.sismics.docs.core.model.jpa.File; + +import java.io.InputStream; +import java.nio.file.Path; +import java.util.List; + +/** + * Cleanup temporary files event. + * + * @author bgamard + */ +public class TemporaryFileCleanupAsyncEvent { + /** + * Temporary files. + */ + private List fileList; + + public TemporaryFileCleanupAsyncEvent(List fileList) { + this.fileList = fileList; + } + + public List getFileList() { + return fileList; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("files", fileList) + .toString(); + } +} \ No newline at end of file diff --git a/docs-core/src/main/java/com/sismics/docs/core/listener/async/FileCreatedAsyncListener.java b/docs-core/src/main/java/com/sismics/docs/core/listener/async/FileCreatedAsyncListener.java index e2244ad5..b2992de4 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/listener/async/FileCreatedAsyncListener.java +++ b/docs-core/src/main/java/com/sismics/docs/core/listener/async/FileCreatedAsyncListener.java @@ -1,10 +1,5 @@ package com.sismics.docs.core.listener.async; -import java.text.MessageFormat; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import com.google.common.eventbus.Subscribe; import com.sismics.docs.core.dao.jpa.FileDao; import com.sismics.docs.core.dao.lucene.LuceneDao; @@ -12,6 +7,10 @@ import com.sismics.docs.core.event.FileCreatedAsyncEvent; import com.sismics.docs.core.model.jpa.File; import com.sismics.docs.core.util.FileUtil; import com.sismics.docs.core.util.TransactionUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.text.MessageFormat; /** * Listener on file created. @@ -28,7 +27,7 @@ public class FileCreatedAsyncListener { * File created. * * @param fileCreatedAsyncEvent File created event - * @throws Exception + * @throws Exception e */ @Subscribe public void on(final FileCreatedAsyncEvent fileCreatedAsyncEvent) throws Exception { @@ -42,11 +41,7 @@ public class FileCreatedAsyncListener { // Extract text content from the file long startTime = System.currentTimeMillis(); final String content = FileUtil.extractContent(fileCreatedAsyncEvent.getLanguage(), file, - fileCreatedAsyncEvent.getInputStream(), fileCreatedAsyncEvent.getPdfInputStream()); - fileCreatedAsyncEvent.getInputStream().close(); - if (fileCreatedAsyncEvent.getPdfInputStream() != null) { - fileCreatedAsyncEvent.getPdfInputStream().close(); - } + fileCreatedAsyncEvent.getUnencryptedFile(), fileCreatedAsyncEvent.getUnencryptedPdfFile()); log.info(MessageFormat.format("File content extracted in {0}ms", System.currentTimeMillis() - startTime)); // Store the text content in the database diff --git a/docs-core/src/main/java/com/sismics/docs/core/listener/async/TemporaryFileCleanupAsyncListener.java b/docs-core/src/main/java/com/sismics/docs/core/listener/async/TemporaryFileCleanupAsyncListener.java new file mode 100644 index 00000000..378afb27 --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/listener/async/TemporaryFileCleanupAsyncListener.java @@ -0,0 +1,38 @@ +package com.sismics.docs.core.listener.async; + +import com.google.common.eventbus.Subscribe; +import com.sismics.docs.core.event.TemporaryFileCleanupAsyncEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.file.Files; +import java.nio.file.Path; + +/** + * Listener to cleanup temporary files created during a request. + * + * @author bgamard + */ +public class TemporaryFileCleanupAsyncListener { + /** + * Logger. + */ + private static final Logger log = LoggerFactory.getLogger(FileCreatedAsyncListener.class); + + /** + * Cleanup temporary files. + * + * @param event Temporary file cleanup event + * @throws Exception + */ + @Subscribe + public void on(final TemporaryFileCleanupAsyncEvent event) throws Exception { + if (log.isInfoEnabled()) { + log.info("Cleanup temporary files event: " + event.toString()); + } + + for (Path file : event.getFileList()) { + Files.delete(file); + } + } +} diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/context/AppContext.java b/docs-core/src/main/java/com/sismics/docs/core/model/context/AppContext.java index 52a02c44..78a7cb45 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/model/context/AppContext.java +++ b/docs-core/src/main/java/com/sismics/docs/core/model/context/AppContext.java @@ -12,12 +12,8 @@ import com.google.common.eventbus.EventBus; import com.lowagie.text.FontFactory; import com.sismics.docs.core.constant.ConfigType; import com.sismics.docs.core.dao.jpa.ConfigDao; -import com.sismics.docs.core.listener.async.DocumentCreatedAsyncListener; -import com.sismics.docs.core.listener.async.DocumentDeletedAsyncListener; -import com.sismics.docs.core.listener.async.DocumentUpdatedAsyncListener; -import com.sismics.docs.core.listener.async.FileCreatedAsyncListener; -import com.sismics.docs.core.listener.async.FileDeletedAsyncListener; -import com.sismics.docs.core.listener.async.RebuildIndexAsyncListener; +import com.sismics.docs.core.event.TemporaryFileCleanupAsyncEvent; +import com.sismics.docs.core.listener.async.*; import com.sismics.docs.core.listener.sync.DeadEventListener; import com.sismics.docs.core.model.jpa.Config; import com.sismics.docs.core.service.IndexingService; @@ -86,6 +82,7 @@ public class AppContext { asyncEventBus.register(new DocumentUpdatedAsyncListener()); asyncEventBus.register(new DocumentDeletedAsyncListener()); asyncEventBus.register(new RebuildIndexAsyncListener()); + asyncEventBus.register(new TemporaryFileCleanupAsyncListener()); } /** @@ -132,6 +129,7 @@ public class AppContext { if (EnvironmentUtil.isUnitTest()) { return new EventBus(); } else { + // /!\ Don't add more threads because a cleanup event is fired at the end of each request ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue()); diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/EncryptionUtil.java b/docs-core/src/main/java/com/sismics/docs/core/util/EncryptionUtil.java index d86d9759..c47883c1 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/util/EncryptionUtil.java +++ b/docs-core/src/main/java/com/sismics/docs/core/util/EncryptionUtil.java @@ -1,20 +1,22 @@ package com.sismics.docs.core.util; -import java.io.InputStream; -import java.math.BigInteger; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.security.Security; +import com.google.common.base.Strings; +import com.sismics.util.context.ThreadLocalContext; +import org.bouncycastle.jce.provider.BouncyCastleProvider; import javax.crypto.Cipher; import javax.crypto.CipherInputStream; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.PBEKeySpec; - -import org.bouncycastle.jce.provider.BouncyCastleProvider; - -import com.google.common.base.Strings; +import java.io.InputStream; +import java.math.BigInteger; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.Security; /** * Encryption utilities. @@ -55,7 +57,28 @@ public class EncryptionUtil { public static InputStream decryptInputStream(InputStream is, String privateKey) throws Exception { return new CipherInputStream(is, getCipher(privateKey, Cipher.DECRYPT_MODE)); } - + + /** + * Decrypt a file to a temporary file using the specified private key. + * + * @param file Encrypted file + * @param privateKey Private key + * @return Decrypted temporary file + * @throws Exception + */ + public static Path decryptFile(Path file, String privateKey) throws Exception { + if (privateKey == null) { + // For unit testing + return file; + } + + Path tmpFile = ThreadLocalContext.get().createTemporaryFile(); + try (InputStream is = Files.newInputStream(file)) { + Files.copy(new CipherInputStream(is, getCipher(privateKey, Cipher.DECRYPT_MODE)), tmpFile, StandardCopyOption.REPLACE_EXISTING); + } + return tmpFile; + } + /** * Return an encryption cipher. * diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/FileUtil.java b/docs-core/src/main/java/com/sismics/docs/core/util/FileUtil.java index 515a0d3f..e81dffae 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/util/FileUtil.java +++ b/docs-core/src/main/java/com/sismics/docs/core/util/FileUtil.java @@ -36,34 +36,34 @@ public class FileUtil { * * @param language Language to extract * @param file File to extract - * @param inputStream Unencrypted input stream - * @param pdfInputStream Unencrypted PDF input stream + * @param unencryptedFile Unencrypted file + * @param unencryptedPdfFile Unencrypted PDF file * @return Content extract */ - public static String extractContent(String language, File file, InputStream inputStream, InputStream pdfInputStream) { + public static String extractContent(String language, File file, Path unencryptedFile, Path unencryptedPdfFile) { String content = null; if (ImageUtil.isImage(file.getMimeType())) { - content = ocrFile(inputStream, language); - } else if (pdfInputStream != null) { - content = PdfUtil.extractPdf(pdfInputStream); + content = ocrFile(unencryptedFile, language); + } else if (unencryptedPdfFile != null) { + content = PdfUtil.extractPdf(unencryptedPdfFile); } return content; } /** - * Optical character recognition on a stream. + * Optical character recognition on a file. * - * @param inputStream Unencrypted input stream + * @param unecryptedFile Unencrypted file * @param language Language to OCR * @return Content extracted */ - private static String ocrFile(InputStream inputStream, String language) { + private static String ocrFile(Path unecryptedFile, String language) { Tesseract instance = Tesseract.getInstance(); String content = null; BufferedImage image; - try { + try (InputStream inputStream = Files.newInputStream(unecryptedFile)) { image = ImageIO.read(inputStream); } catch (IOException e) { log.error("Error reading the image", e); @@ -90,38 +90,39 @@ public class FileUtil { /** * Save a file on the storage filesystem. * - * @param inputStream Unencrypted input stream - * @param pdfInputStream PDF input stream + * @param unencryptedFile Unencrypted file + * @param unencryptedPdfFile Unencrypted PDF file * @param file File to save * @param privateKey Private key used for encryption */ - public static void save(InputStream inputStream, InputStream pdfInputStream, File file, String privateKey) throws Exception { + public static void save(Path unencryptedFile, Path unencryptedPdfFile, File file, String privateKey) throws Exception { Cipher cipher = EncryptionUtil.getEncryptionCipher(privateKey); Path path = DirectoryUtil.getStorageDirectory().resolve(file.getId()); - Files.copy(new CipherInputStream(inputStream, cipher), path); - inputStream.reset(); - + try (InputStream inputStream = Files.newInputStream(unencryptedFile)) { + Files.copy(new CipherInputStream(inputStream, cipher), path); + } + // Generate file variations - saveVariations(file, inputStream, pdfInputStream, cipher); + saveVariations(file, unencryptedFile, unencryptedPdfFile, cipher); } /** * Generate file variations. * * @param file File from database - * @param inputStream Unencrypted input stream - * @param pdfInputStream Unencrypted PDF input stream + * @param unencryptedFile Unencrypted file + * @param unencryptedPdfFile Unencrypted PDF file * @param cipher Cipher to use for encryption */ - private static void saveVariations(File file, InputStream inputStream, InputStream pdfInputStream, Cipher cipher) throws Exception { + private static void saveVariations(File file, Path unencryptedFile, Path unencryptedPdfFile, Cipher cipher) throws Exception { BufferedImage image = null; if (ImageUtil.isImage(file.getMimeType())) { - image = ImageIO.read(inputStream); - inputStream.reset(); - } else if(pdfInputStream != null) { + try (InputStream inputStream = Files.newInputStream(unencryptedFile)) { + image = ImageIO.read(inputStream); + } + } else if (unencryptedPdfFile != null) { // Generate preview from the first page of the PDF - image = PdfUtil.renderFirstPage(pdfInputStream); - pdfInputStream.reset(); + image = PdfUtil.renderFirstPage(unencryptedPdfFile); } if (image != null) { diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/PdfUtil.java b/docs-core/src/main/java/com/sismics/docs/core/util/PdfUtil.java index 38b2b074..71a4eaaf 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/util/PdfUtil.java +++ b/docs-core/src/main/java/com/sismics/docs/core/util/PdfUtil.java @@ -3,7 +3,6 @@ package com.sismics.docs.core.util; import com.google.common.base.Charsets; import com.google.common.base.Strings; import com.google.common.io.ByteStreams; -import com.google.common.io.CharStreams; import com.google.common.io.Closer; import com.google.common.io.Resources; import com.lowagie.text.*; @@ -12,6 +11,7 @@ import com.sismics.docs.core.dao.jpa.dto.DocumentDto; import com.sismics.docs.core.model.jpa.File; import com.sismics.docs.core.util.pdf.PdfPage; import com.sismics.util.ImageUtil; +import com.sismics.util.context.ThreadLocalContext; import com.sismics.util.mime.MimeType; import org.apache.pdfbox.io.MemoryUsageSetting; import org.apache.pdfbox.multipdf.PDFMergerUtility; @@ -34,7 +34,9 @@ import org.slf4j.LoggerFactory; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; -import java.io.*; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; @@ -56,13 +58,13 @@ public class PdfUtil { /** * Extract text from a PDF. * - * @param inputStream Unencrypted input stream + * @param unencryptedPdfFile Unencrypted PDF file * @return Content extracted */ - public static String extractPdf(InputStream inputStream) { + public static String extractPdf(Path unencryptedPdfFile) { String content = null; PDDocument pdfDocument = null; - try { + try (InputStream inputStream = Files.newInputStream(unencryptedPdfFile)) { PDFTextStripper stripper = new PDFTextStripper(); pdfDocument = PDDocument.load(inputStream); content = stripper.getText(pdfDocument); @@ -85,26 +87,25 @@ public class PdfUtil { * Convert a file to PDF if necessary. * * @param file File - * @param inputStream InputStream - * @param reset Reset the stream after usage - * @return PDF input stream + * @param unencryptedFile Unencrypted file + * @return PDF temporary file */ - public static InputStream convertToPdf(File file, InputStream inputStream, boolean reset) throws Exception { + public static Path convertToPdf(File file, Path unencryptedFile) throws Exception { if (file.getMimeType().equals(MimeType.APPLICATION_PDF)) { - // It's already PDF, just return the input - return inputStream; + // It's already PDF, just return the file + return unencryptedFile; } if (file.getMimeType().equals(MimeType.OFFICE_DOCUMENT)) { - return convertOfficeDocument(inputStream, reset); + return convertOfficeDocument(unencryptedFile); } if (file.getMimeType().equals(MimeType.OPEN_DOCUMENT_TEXT)) { - return convertOpenDocumentText(inputStream, reset); + return convertOpenDocumentText(unencryptedFile); } if (file.getMimeType().equals(MimeType.TEXT_PLAIN) || file.getMimeType().equals(MimeType.TEXT_CSV)) { - return convertTextPlain(inputStream, reset); + return convertTextPlain(unencryptedFile); } // PDF conversion not necessary/possible @@ -114,64 +115,58 @@ public class PdfUtil { /** * Convert a text plain document to PDF. * - * @param inputStream Unecnrypted input stream - * @param reset Reset the stream after usage - * @return PDF input stream + * @param unencryptedFile Unencrypted file + * @return PDF file */ - private static InputStream convertTextPlain(InputStream inputStream, boolean reset) throws Exception { + private static Path convertTextPlain(Path unencryptedFile) throws Exception { Document output = new Document(PageSize.A4, 40, 40, 40, 40); - ByteArrayOutputStream pdfOutputStream = new ByteArrayOutputStream(); + Path tempFile = ThreadLocalContext.get().createTemporaryFile(); + OutputStream pdfOutputStream = Files.newOutputStream(tempFile); PdfWriter.getInstance(output, pdfOutputStream); output.open(); - String content = CharStreams.toString(new InputStreamReader(inputStream, Charsets.UTF_8)); + String content = new String(Files.readAllBytes(unencryptedFile), Charsets.UTF_8); Font font = FontFactory.getFont("LiberationMono-Regular"); Paragraph paragraph = new Paragraph(content, font); paragraph.setAlignment(Element.ALIGN_LEFT); output.add(paragraph); output.close(); - if (reset) { - inputStream.reset(); - } - - return new ByteArrayInputStream(pdfOutputStream.toByteArray()); + return tempFile; } /** * Convert an open document text file to PDF. * - * @param inputStream Unencrypted input stream - * @param reset Reset the stream after usage - * @return PDF input stream + * @param unencryptedFile Unencrypted file + * @return PDF file */ - private static InputStream convertOpenDocumentText(InputStream inputStream, boolean reset) throws Exception { - ByteArrayOutputStream pdfOutputStream = new ByteArrayOutputStream(); - OdfTextDocument document = OdfTextDocument.loadDocument(inputStream); - PdfOptions options = PdfOptions.create(); - PdfConverter.getInstance().convert(document, pdfOutputStream, options); - if (reset) { - inputStream.reset(); + private static Path convertOpenDocumentText(Path unencryptedFile) throws Exception { + Path tempFile = ThreadLocalContext.get().createTemporaryFile(); + try (InputStream inputStream = Files.newInputStream(unencryptedFile); + OutputStream outputStream = Files.newOutputStream(tempFile)) { + OdfTextDocument document = OdfTextDocument.loadDocument(inputStream); + PdfOptions options = PdfOptions.create(); + PdfConverter.getInstance().convert(document, outputStream, options); } - return new ByteArrayInputStream(pdfOutputStream.toByteArray()); + return tempFile; } /** * Convert an Office document to PDF. * - * @param inputStream Unencrypted input stream - * @param reset Reset the stream after usage - * @return PDF input stream + * @param unencryptedFile Unencrypted file + * @return PDF file */ - private static InputStream convertOfficeDocument(InputStream inputStream, boolean reset) throws Exception { - ByteArrayOutputStream pdfOutputStream = new ByteArrayOutputStream(); - XWPFDocument document = new XWPFDocument(inputStream); - org.apache.poi.xwpf.converter.pdf.PdfOptions options = org.apache.poi.xwpf.converter.pdf.PdfOptions.create(); - org.apache.poi.xwpf.converter.pdf.PdfConverter.getInstance().convert(document, pdfOutputStream, options); - if (reset) { - inputStream.reset(); + private static Path convertOfficeDocument(Path unencryptedFile) throws Exception { + Path tempFile = ThreadLocalContext.get().createTemporaryFile(); + try (InputStream inputStream = Files.newInputStream(unencryptedFile); + OutputStream outputStream = Files.newOutputStream(tempFile)) { + XWPFDocument document = new XWPFDocument(inputStream); + org.apache.poi.xwpf.converter.pdf.PdfOptions options = org.apache.poi.xwpf.converter.pdf.PdfOptions.create(); + org.apache.poi.xwpf.converter.pdf.PdfConverter.getInstance().convert(document, outputStream, options); } - return new ByteArrayInputStream(pdfOutputStream.toByteArray()); + return tempFile; } /** @@ -182,10 +177,10 @@ public class PdfUtil { * @param fitImageToPage Fit images to the page * @param metadata Add a page with metadata * @param margin Margins in millimeters - * @return PDF input stream + * @param outputStream Output stream to write to, will be closed */ - public static InputStream convertToPdf(DocumentDto documentDto, List fileList, - boolean fitImageToPage, boolean metadata, int margin) throws Exception { + public static void convertToPdf(DocumentDto documentDto, List fileList, + boolean fitImageToPage, boolean metadata, int margin, OutputStream outputStream) throws Exception { // Setup PDFBox Closer closer = Closer.create(); MemoryUsageSetting memUsageSettings = MemoryUsageSetting.setupMixed(1000000); // 1MB max memory usage @@ -240,80 +235,75 @@ public class PdfUtil { // Add files for (File file : fileList) { Path storedFile = DirectoryUtil.getStorageDirectory().resolve(file.getId()); - try (InputStream storedFileInputStream = file.getPrivateKey() == null ? // Try to decrypt the file if we have a private key available - Files.newInputStream(storedFile) : EncryptionUtil.decryptInputStream(Files.newInputStream(storedFile), file.getPrivateKey())) { - if (ImageUtil.isImage(file.getMimeType())) { - PDPage page = new PDPage(PDRectangle.A4); // Images into A4 pages - try (PDPageContentStream contentStream = new PDPageContentStream(doc, page)) { - // Read the image using the correct handler. PDFBox can't do it because it relies wrongly on file extension - PDImageXObject pdImage = null; - if (file.getMimeType().equals(MimeType.IMAGE_JPEG)) { - pdImage = JPEGFactory.createFromStream(doc, storedFileInputStream); - } else if (file.getMimeType().equals(MimeType.IMAGE_GIF) || file.getMimeType().equals(MimeType.IMAGE_PNG)) { - BufferedImage bim = ImageIO.read(storedFileInputStream); - pdImage = LosslessFactory.createFromImage(doc, bim); - } - - // Do we want to fill the page with the image? - if (fitImageToPage) { - // Fill the page with the image - float widthAvailable = page.getMediaBox().getWidth() - 2 * margin * mmPerInch; - float heightAvailable = page.getMediaBox().getHeight() - 2 * margin * mmPerInch; - - // Compare page format and image format - if (widthAvailable / heightAvailable < (float) pdImage.getWidth() / (float) pdImage.getHeight()) { - float imageHeight = widthAvailable / pdImage.getWidth() * pdImage.getHeight(); - contentStream.drawImage(pdImage, margin * mmPerInch, heightAvailable + margin * mmPerInch - imageHeight, - widthAvailable, imageHeight); - } else { - float imageWidth = heightAvailable / pdImage.getHeight() * pdImage.getWidth(); - contentStream.drawImage(pdImage, margin * mmPerInch, margin * mmPerInch, - imageWidth, heightAvailable); - } + + // Decrypt the file to a temporary file + Path unencryptedFile = EncryptionUtil.decryptFile(storedFile, file.getPrivateKey()); + + if (ImageUtil.isImage(file.getMimeType())) { + PDPage page = new PDPage(PDRectangle.A4); // Images into A4 pages + try (PDPageContentStream contentStream = new PDPageContentStream(doc, page); + InputStream storedFileInputStream = Files.newInputStream(unencryptedFile)) { + // Read the image using the correct handler. PDFBox can't do it because it relies wrongly on file extension + PDImageXObject pdImage = null; + if (file.getMimeType().equals(MimeType.IMAGE_JPEG)) { + pdImage = JPEGFactory.createFromStream(doc, storedFileInputStream); + } else if (file.getMimeType().equals(MimeType.IMAGE_GIF) || file.getMimeType().equals(MimeType.IMAGE_PNG)) { + BufferedImage bim = ImageIO.read(storedFileInputStream); + pdImage = LosslessFactory.createFromImage(doc, bim); + } + + // Do we want to fill the page with the image? + if (fitImageToPage) { + // Fill the page with the image + float widthAvailable = page.getMediaBox().getWidth() - 2 * margin * mmPerInch; + float heightAvailable = page.getMediaBox().getHeight() - 2 * margin * mmPerInch; + + // Compare page format and image format + if (widthAvailable / heightAvailable < (float) pdImage.getWidth() / (float) pdImage.getHeight()) { + float imageHeight = widthAvailable / pdImage.getWidth() * pdImage.getHeight(); + contentStream.drawImage(pdImage, margin * mmPerInch, heightAvailable + margin * mmPerInch - imageHeight, + widthAvailable, imageHeight); } else { - // Draw the image as is - contentStream.drawImage(pdImage, margin * mmPerInch, - page.getMediaBox().getHeight() - pdImage.getHeight() - margin * mmPerInch); + float imageWidth = heightAvailable / pdImage.getHeight() * pdImage.getWidth(); + contentStream.drawImage(pdImage, margin * mmPerInch, margin * mmPerInch, + imageWidth, heightAvailable); } + } else { + // Draw the image as is + contentStream.drawImage(pdImage, margin * mmPerInch, + page.getMediaBox().getHeight() - pdImage.getHeight() - margin * mmPerInch); } - doc.addPage(page); - } else { - // Try to convert the file to PDF - InputStream pdfInputStream = convertToPdf(file, storedFileInputStream, false); - if (pdfInputStream != null) { - // This file is convertible to PDF, just add it to the end - try { - PDDocument mergeDoc = PDDocument.load(pdfInputStream, memUsageSettings); - closer.register(mergeDoc); - PDFMergerUtility pdfMergerUtility = new PDFMergerUtility(); - pdfMergerUtility.appendDocument(doc, mergeDoc); - } finally { - pdfInputStream.close(); - } - } - - // All other non-PDF-convertible files are ignored } + doc.addPage(page); + } else { + // Try to convert the file to PDF + Path unencryptedPdfFile = convertToPdf(file, unencryptedFile); + if (unencryptedPdfFile != null) { + // This file is convertible to PDF, just add it to the end + PDDocument mergeDoc = PDDocument.load(unencryptedPdfFile.toFile(), memUsageSettings); + closer.register(mergeDoc); + PDFMergerUtility pdfMergerUtility = new PDFMergerUtility(); + pdfMergerUtility.appendDocument(doc, mergeDoc); + } + + // All other non-PDF-convertible files are ignored } } - // Save to a temporary file - try (TemporaryFileStream temporaryFileStream = new TemporaryFileStream()) { - doc.save(temporaryFileStream.openWriteStream()); - closer.close(); // Close all remaining opened PDF - return temporaryFileStream.openReadStream(); - } + doc.save(outputStream); // Write to the output stream + closer.close(); // Close all remaining opened PDF } } /** * Render the first page of a PDF. * - * @param inputStream PDF document + * @param unencryptedFile PDF document * @return Render of the first page */ - public static BufferedImage renderFirstPage(InputStream inputStream) throws IOException { - try (PDDocument pdfDocument = PDDocument.load(inputStream)) { + public static BufferedImage renderFirstPage(Path unencryptedFile) throws IOException { + try (InputStream inputStream = Files.newInputStream(unencryptedFile); + PDDocument pdfDocument = PDDocument.load(inputStream)) { PDFRenderer renderer = new PDFRenderer(pdfDocument); return renderer.renderImage(0); } diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/TemporaryFileStream.java b/docs-core/src/main/java/com/sismics/docs/core/util/TemporaryFileStream.java deleted file mode 100644 index 9b357f05..00000000 --- a/docs-core/src/main/java/com/sismics/docs/core/util/TemporaryFileStream.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.sismics.docs.core.util; - -import java.io.Closeable; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.UUID; - -/** - * Utilities for writing and reading to a temporary file. - * - * @author bgamard - */ -public class TemporaryFileStream implements Closeable { - /** - * Temporary file. - */ - private Path tempFile; - - /** - * Construct a temporary file. - * - * @throws IOException - */ - public TemporaryFileStream() throws IOException { - tempFile = Files.createTempFile(UUID.randomUUID().toString(), ".tmp"); - } - - /** - * Open a stream for writing. - * - * @return OutputStream - * @throws IOException - */ - public OutputStream openWriteStream() throws IOException { - return Files.newOutputStream(tempFile); - } - - /** - * Open a stream for reading. - * - * @return InputStream - * @throws IOException - */ - public InputStream openReadStream() throws IOException { - return Files.newInputStream(tempFile); - } - - @Override - public void close() throws IOException { - Files.delete(tempFile); - } -} \ No newline at end of file diff --git a/docs-core/src/main/java/com/sismics/util/context/ThreadLocalContext.java b/docs-core/src/main/java/com/sismics/util/context/ThreadLocalContext.java index 4270c627..5d0049be 100644 --- a/docs-core/src/main/java/com/sismics/util/context/ThreadLocalContext.java +++ b/docs-core/src/main/java/com/sismics/util/context/ThreadLocalContext.java @@ -1,9 +1,13 @@ package com.sismics.util.context; import com.google.common.collect.Lists; +import com.sismics.docs.core.event.TemporaryFileCleanupAsyncEvent; import com.sismics.docs.core.model.context.AppContext; import javax.persistence.EntityManager; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.List; /** @@ -27,6 +31,12 @@ public class ThreadLocalContext { */ private List asyncEventList = Lists.newArrayList(); + /** + * List of temporary files created during this request. + * They are deleted at the end of each request. + */ + private List temporaryFileList = Lists.newArrayList(); + /** * Private constructor. */ @@ -82,6 +92,17 @@ public class ThreadLocalContext { asyncEventList.add(asyncEvent); } + /** + * Create a temporary file linked to the request. + * + * @return New temporary file + */ + public Path createTemporaryFile() throws IOException { + Path path = Files.createTempFile("sismics_docs", null); + temporaryFileList.add(path); + return path; + } + /** * Fire all pending async events. */ @@ -89,5 +110,11 @@ public class ThreadLocalContext { for (Object asyncEvent : asyncEventList) { AppContext.getInstance().getAsyncEventBus().post(asyncEvent); } + + if (!temporaryFileList.isEmpty()) { + // Some files were created during this request, add a cleanup event to the queue + // It works because we are using a one thread executor + AppContext.getInstance().getAsyncEventBus().post(new TemporaryFileCleanupAsyncEvent(temporaryFileList)); + } } } diff --git a/docs-core/src/main/java/com/sismics/util/mime/MimeTypeUtil.java b/docs-core/src/main/java/com/sismics/util/mime/MimeTypeUtil.java index 740a6c7c..52d5e1df 100644 --- a/docs-core/src/main/java/com/sismics/util/mime/MimeTypeUtil.java +++ b/docs-core/src/main/java/com/sismics/util/mime/MimeTypeUtil.java @@ -3,6 +3,10 @@ package com.sismics.util.mime; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; import org.apache.commons.compress.archivers.ArchiveEntry; import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream; @@ -20,17 +24,17 @@ public class MimeTypeUtil { /** * Try to guess the MIME type of a file by its magic number (header). * - * @param is Stream to inspect + * @param file File to inspect * @param name File name * @return MIME type * @throws IOException e */ - public static String guessMimeType(InputStream is, String name) throws IOException { - byte[] headerBytes = new byte[64]; - is.mark(headerBytes.length); - is.read(headerBytes); - is.reset(); - return guessMimeType(headerBytes, name); + public static String guessMimeType(Path file, String name) throws IOException { + try (InputStream is = Files.newInputStream(file)) { + byte[] headerBytes = new byte[64]; + is.read(headerBytes); + return guessMimeType(headerBytes, name); + } } /** @@ -107,39 +111,38 @@ public class MimeTypeUtil { * are simple ZIP files on the outside and much bigger on the inside. * * @param file File - * @param inputStream Input stream + * @param unencryptedFile File on disk * @return MIME type */ - public static String guessOpenDocumentFormat(File file, InputStream inputStream) { + public static String guessOpenDocumentFormat(File file, Path unencryptedFile) { if (!MimeType.APPLICATION_ZIP.equals(file.getMimeType())) { // open document formats are ZIP files return file.getMimeType(); } String mimeType = file.getMimeType(); - try (ZipArchiveInputStream archiveInputStream = new ZipArchiveInputStream(inputStream, Charsets.ISO_8859_1.name())) { - ArchiveEntry archiveEntry = archiveInputStream.getNextEntry(); + try (InputStream inputStream = Files.newInputStream(unencryptedFile); + ZipInputStream zipInputStream = new ZipInputStream(inputStream, Charsets.ISO_8859_1)) { + ZipEntry archiveEntry = zipInputStream.getNextEntry(); while (archiveEntry != null) { if (archiveEntry.getName().equals("mimetype")) { // Maybe it's an ODT file - String content = new String(IOUtils.toByteArray(archiveInputStream), Charsets.ISO_8859_1); + String content = new String(IOUtils.toByteArray(zipInputStream), Charsets.ISO_8859_1); if (MimeType.OPEN_DOCUMENT_TEXT.equals(content.trim())) { mimeType = MimeType.OPEN_DOCUMENT_TEXT; break; } } else if (archiveEntry.getName().equals("[Content_Types].xml")) { // Maybe it's a DOCX file - String content = new String(IOUtils.toByteArray(archiveInputStream), Charsets.ISO_8859_1); + String content = new String(IOUtils.toByteArray(zipInputStream), Charsets.ISO_8859_1); if (content.contains(MimeType.OFFICE_DOCUMENT)) { mimeType = MimeType.OFFICE_DOCUMENT; break; } } - archiveEntry = archiveInputStream.getNextEntry(); + archiveEntry = zipInputStream.getNextEntry(); } - - inputStream.reset(); } catch (Exception e) { // In case of any error, just give up and keep the ZIP MIME type return file.getMimeType(); diff --git a/docs-core/src/test/java/com/sismics/docs/core/util/TestFileUtil.java b/docs-core/src/test/java/com/sismics/docs/core/util/TestFileUtil.java index 4f7a58d9..0417f691 100644 --- a/docs-core/src/test/java/com/sismics/docs/core/util/TestFileUtil.java +++ b/docs-core/src/test/java/com/sismics/docs/core/util/TestFileUtil.java @@ -1,18 +1,20 @@ package com.sismics.docs.core.util; -import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.StandardCopyOption; -import java.util.Date; - -import org.junit.Assert; -import org.junit.Test; - import com.google.common.collect.Lists; import com.google.common.io.Resources; import com.sismics.docs.core.dao.jpa.dto.DocumentDto; import com.sismics.docs.core.model.jpa.File; import com.sismics.util.mime.MimeType; +import org.junit.Assert; +import org.junit.Test; + +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.Date; /** * Test of the file entity utilities. @@ -22,26 +24,22 @@ import com.sismics.util.mime.MimeType; public class TestFileUtil { @Test public void extractContentOpenDocumentTextTest() throws Exception { - try (InputStream inputStream = Resources.getResource("file/document.odt").openStream()) { - File file = new File(); - file.setMimeType(MimeType.OPEN_DOCUMENT_TEXT); - try (InputStream pdfInputStream = PdfUtil.convertToPdf(file, inputStream, false)) { - String content = FileUtil.extractContent(null, file, inputStream, pdfInputStream); - Assert.assertTrue(content.contains("Lorem ipsum dolor sit amen.")); - } - } + Path path = Paths.get(ClassLoader.getSystemResource("file/document.odt").toURI()); + File file = new File(); + file.setMimeType(MimeType.OPEN_DOCUMENT_TEXT); + Path pdfPath = PdfUtil.convertToPdf(file, path); + String content = FileUtil.extractContent(null, file, path, pdfPath); + Assert.assertTrue(content.contains("Lorem ipsum dolor sit amen.")); } @Test public void extractContentOfficeDocumentTest() throws Exception { - try (InputStream inputStream = Resources.getResource("file/document.docx").openStream()) { - File file = new File(); - file.setMimeType(MimeType.OFFICE_DOCUMENT); - try (InputStream pdfInputStream = PdfUtil.convertToPdf(file, inputStream, false)) { - String content = FileUtil.extractContent(null, file, inputStream, pdfInputStream); - Assert.assertTrue(content.contains("Lorem ipsum dolor sit amen.")); - } - } + Path path = Paths.get(ClassLoader.getSystemResource("file/document.docx").toURI()); + File file = new File(); + file.setMimeType(MimeType.OFFICE_DOCUMENT); + Path pdfPath = PdfUtil.convertToPdf(file, path); + String content = FileUtil.extractContent(null, file, path, pdfPath); + Assert.assertTrue(content.contains("Lorem ipsum dolor sit amen.")); } @Test @@ -97,8 +95,9 @@ public class TestFileUtil { file4.setId("document_odt"); file4.setMimeType(MimeType.OPEN_DOCUMENT_TEXT); - InputStream is = PdfUtil.convertToPdf(documentDto, Lists.newArrayList(file0, file1, file2, file3, file4), true, true, 10); - is.close(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + PdfUtil.convertToPdf(documentDto, Lists.newArrayList(file0, file1, file2, file3, file4), true, true, 10, outputStream); + Assert.assertTrue(outputStream.toByteArray().length > 0); } } } diff --git a/docs-core/src/test/java/com/sismics/util/TestMimeTypeUtil.java b/docs-core/src/test/java/com/sismics/util/TestMimeTypeUtil.java index b16e91c7..06a0e159 100644 --- a/docs-core/src/test/java/com/sismics/util/TestMimeTypeUtil.java +++ b/docs-core/src/test/java/com/sismics/util/TestMimeTypeUtil.java @@ -1,16 +1,13 @@ package com.sismics.util; -import java.io.ByteArrayInputStream; -import java.io.InputStream; - -import org.apache.commons.compress.utils.IOUtils; -import org.junit.Assert; -import org.junit.Test; - -import com.google.common.io.Resources; import com.sismics.docs.core.model.jpa.File; import com.sismics.util.mime.MimeType; import com.sismics.util.mime.MimeTypeUtil; +import org.junit.Assert; +import org.junit.Test; + +import java.nio.file.Path; +import java.nio.file.Paths; /** * Test of the utilities to check MIME types. @@ -18,23 +15,18 @@ import com.sismics.util.mime.MimeTypeUtil; * @author bgamard */ public class TestMimeTypeUtil { - @Test public void guessOpenDocumentFormatTest() throws Exception { // Detect ODT files - try (InputStream inputStream = Resources.getResource("file/document.odt").openStream(); - InputStream byteArrayInputStream = new ByteArrayInputStream(IOUtils.toByteArray(inputStream))) { - File file = new File(); - file.setMimeType(MimeType.APPLICATION_ZIP); - Assert.assertEquals(MimeType.OPEN_DOCUMENT_TEXT, MimeTypeUtil.guessOpenDocumentFormat(file, byteArrayInputStream)); - } - + Path path = Paths.get(ClassLoader.getSystemResource("file/document.odt").toURI()); + File file = new File(); + file.setMimeType(MimeType.APPLICATION_ZIP); + Assert.assertEquals(MimeType.OPEN_DOCUMENT_TEXT, MimeTypeUtil.guessOpenDocumentFormat(file, path)); + // Detect DOCX files - try (InputStream inputStream = Resources.getResource("file/document.docx").openStream(); - InputStream byteArrayInputStream = new ByteArrayInputStream(IOUtils.toByteArray(inputStream))) { - File file = new File(); - file.setMimeType(MimeType.APPLICATION_ZIP); - Assert.assertEquals(MimeType.OFFICE_DOCUMENT, MimeTypeUtil.guessOpenDocumentFormat(file, byteArrayInputStream)); - } + path = Paths.get(ClassLoader.getSystemResource("file/document.docx").toURI()); + file = new File(); + file.setMimeType(MimeType.APPLICATION_ZIP); + Assert.assertEquals(MimeType.OFFICE_DOCUMENT, MimeTypeUtil.guessOpenDocumentFormat(file, path)); } } diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/DocumentResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/DocumentResource.java index bda72709..6d692aa1 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/DocumentResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/DocumentResource.java @@ -275,16 +275,10 @@ public class DocumentResource extends BaseResource { StreamingOutput stream = new StreamingOutput() { @Override public void write(OutputStream outputStream) throws IOException, WebApplicationException { - try (InputStream inputStream = PdfUtil.convertToPdf(documentDto, fileList, fitImageToPage, metadata, margin)) { - ByteStreams.copy(inputStream, outputStream); + try { + PdfUtil.convertToPdf(documentDto, fileList, fitImageToPage, metadata, margin, outputStream); } catch (Exception e) { throw new IOException(e); - } finally { - try { - outputStream.close(); - } catch (IOException e) { - // Ignore - } } } }; diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java index eb872a75..e88491a0 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java @@ -14,10 +14,7 @@ import com.sismics.docs.core.event.FileCreatedAsyncEvent; import com.sismics.docs.core.event.FileDeletedAsyncEvent; import com.sismics.docs.core.model.jpa.File; import com.sismics.docs.core.model.jpa.User; -import com.sismics.docs.core.util.DirectoryUtil; -import com.sismics.docs.core.util.EncryptionUtil; -import com.sismics.docs.core.util.FileUtil; -import com.sismics.docs.core.util.PdfUtil; +import com.sismics.docs.core.util.*; import com.sismics.rest.exception.ClientException; import com.sismics.rest.exception.ForbiddenClientException; import com.sismics.rest.exception.ServerException; @@ -37,13 +34,11 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.StreamingOutput; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; +import java.io.*; import java.net.URISyntaxException; import java.nio.file.Files; import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; import java.text.MessageFormat; import java.text.SimpleDateFormat; import java.util.Date; @@ -114,27 +109,29 @@ public class FileResource extends BaseResource { } } - // Keep unencrypted data in memory, because we will need it two times - byte[] fileData; + // Keep unencrypted data temporary on disk, because we will need it two times + java.nio.file.Path unencryptedFile; + long fileSize; try { - fileData = ByteStreams.toByteArray(fileBodyPart.getValueAs(InputStream.class)); + unencryptedFile = ThreadLocalContext.get().createTemporaryFile(); + Files.copy(fileBodyPart.getValueAs(InputStream.class), unencryptedFile, StandardCopyOption.REPLACE_EXISTING); + fileSize = Files.size(unencryptedFile); } catch (IOException e) { throw new ServerException("StreamError", "Error reading the input file", e); } - InputStream fileInputStream = new ByteArrayInputStream(fileData); - + // Validate mime type String name = fileBodyPart.getContentDisposition() != null ? fileBodyPart.getContentDisposition().getFileName() : null; String mimeType; try { - mimeType = MimeTypeUtil.guessMimeType(fileInputStream, name); + mimeType = MimeTypeUtil.guessMimeType(unencryptedFile, name); } catch (IOException e) { throw new ServerException("ErrorGuessMime", "Error guessing mime type", e); } // Validate quota - if (user.getStorageCurrent() + fileData.length > user.getStorageQuota()) { + if (user.getStorageCurrent() + fileSize > user.getStorageQuota()) { throw new ClientException("QuotaReached", "Quota limit reached"); } @@ -158,16 +155,16 @@ public class FileResource extends BaseResource { String fileId = fileDao.create(file, principal.getId()); // Guess the mime type a second time, for open document format (first detected as simple ZIP file) - file.setMimeType(MimeTypeUtil.guessOpenDocumentFormat(file, fileInputStream)); - + file.setMimeType(MimeTypeUtil.guessOpenDocumentFormat(file, unencryptedFile)); + // Convert to PDF if necessary (for thumbnail and text extraction) - InputStream pdfIntputStream = PdfUtil.convertToPdf(file, fileInputStream, true); - + java.nio.file.Path unencryptedPdfFile = PdfUtil.convertToPdf(file, unencryptedFile); + // Save the file - FileUtil.save(fileInputStream, pdfIntputStream, file, user.getPrivateKey()); + FileUtil.save(unencryptedFile, unencryptedPdfFile, file, user.getPrivateKey()); // Update the user quota - user.setStorageCurrent(user.getStorageCurrent() + fileData.length); + user.setStorageCurrent(user.getStorageCurrent() + fileSize); userDao.updateQuota(user); // Raise a new file created event and document updated event if we have a document @@ -176,8 +173,8 @@ public class FileResource extends BaseResource { fileCreatedAsyncEvent.setUserId(principal.getId()); fileCreatedAsyncEvent.setLanguage(documentDto.getLanguage()); fileCreatedAsyncEvent.setFile(file); - fileCreatedAsyncEvent.setInputStream(fileInputStream); - fileCreatedAsyncEvent.setPdfInputStream(pdfIntputStream); + fileCreatedAsyncEvent.setUnencryptedFile(unencryptedFile); + fileCreatedAsyncEvent.setUnencryptedPdfFile(unencryptedPdfFile); ThreadLocalContext.get().addAsyncEvent(fileCreatedAsyncEvent); DocumentUpdatedAsyncEvent documentUpdatedAsyncEvent = new DocumentUpdatedAsyncEvent(); @@ -190,7 +187,7 @@ public class FileResource extends BaseResource { JsonObjectBuilder response = Json.createObjectBuilder() .add("status", "ok") .add("id", fileId) - .add("size", fileData.length); + .add("size", fileSize); return Response.ok().entity(response.build()).build(); } catch (Exception e) { throw new ServerException("FileError", "Error adding a file", e); @@ -254,13 +251,13 @@ public class FileResource extends BaseResource { // Raise a new file created event and document updated event (it wasn't sent during file creation) try { java.nio.file.Path storedFile = DirectoryUtil.getStorageDirectory().resolve(id); - InputStream fileInputStream = Files.newInputStream(storedFile); - final InputStream responseInputStream = EncryptionUtil.decryptInputStream(fileInputStream, user.getPrivateKey()); + java.nio.file.Path unencryptedFile = EncryptionUtil.decryptFile(storedFile, user.getPrivateKey()); FileCreatedAsyncEvent fileCreatedAsyncEvent = new FileCreatedAsyncEvent(); fileCreatedAsyncEvent.setUserId(principal.getId()); fileCreatedAsyncEvent.setLanguage(documentDto.getLanguage()); fileCreatedAsyncEvent.setFile(file); - fileCreatedAsyncEvent.setInputStream(responseInputStream); + fileCreatedAsyncEvent.setUnencryptedFile(unencryptedFile); + fileCreatedAsyncEvent.setUnencryptedPdfFile(PdfUtil.convertToPdf(file, unencryptedFile)); ThreadLocalContext.get().addAsyncEvent(fileCreatedAsyncEvent); DocumentUpdatedAsyncEvent documentUpdatedAsyncEvent = new DocumentUpdatedAsyncEvent(); diff --git a/docs-web/src/main/webapp/src/locale/fr.json b/docs-web/src/main/webapp/src/locale/fr.json index 3bb6ce67..95195578 100644 --- a/docs-web/src/main/webapp/src/locale/fr.json +++ b/docs-web/src/main/webapp/src/locale/fr.json @@ -332,7 +332,7 @@ "filter": { "filesize": { "mb": "Mo", - "kb": "Ko" + "kb": "ko" } }, "acl": { diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestFileResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestFileResource.java index 13f70144..0a5102fe 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestFileResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestFileResource.java @@ -1,10 +1,16 @@ package com.sismics.docs.rest; -import java.io.BufferedInputStream; -import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Date; +import com.google.common.io.ByteStreams; +import com.google.common.io.Resources; +import com.sismics.docs.core.util.DirectoryUtil; +import com.sismics.util.filter.TokenBasedSecurityFilter; +import com.sismics.util.mime.MimeType; +import com.sismics.util.mime.MimeTypeUtil; +import org.glassfish.jersey.media.multipart.FormDataMultiPart; +import org.glassfish.jersey.media.multipart.MultiPartFeature; +import org.glassfish.jersey.media.multipart.file.StreamDataBodyPart; +import org.junit.Assert; +import org.junit.Test; import javax.json.JsonArray; import javax.json.JsonObject; @@ -13,19 +19,10 @@ import javax.ws.rs.core.Form; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; - -import org.glassfish.jersey.media.multipart.FormDataMultiPart; -import org.glassfish.jersey.media.multipart.MultiPartFeature; -import org.glassfish.jersey.media.multipart.file.StreamDataBodyPart; -import org.junit.Assert; -import org.junit.Test; - -import com.google.common.io.ByteStreams; -import com.google.common.io.Resources; -import com.sismics.docs.core.util.DirectoryUtil; -import com.sismics.util.filter.TokenBasedSecurityFilter; -import com.sismics.util.mime.MimeType; -import com.sismics.util.mime.MimeTypeUtil; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Date; /** * Exhaustive test of the file resource. @@ -123,10 +120,8 @@ public class TestFileResource extends BaseJerseyTest { // Check that the files are not readable directly from FS Path storedFile = DirectoryUtil.getStorageDirectory().resolve(file1Id); - try (InputStream storedFileInputStream = new BufferedInputStream(Files.newInputStream(storedFile))) { - Assert.assertEquals(MimeType.DEFAULT, MimeTypeUtil.guessMimeType(storedFileInputStream, null)); - } - + Assert.assertEquals(MimeType.DEFAULT, MimeTypeUtil.guessMimeType(storedFile, null)); + // Get all files from a document json = target().path("/file/list") .queryParam("id", document1Id) From f57cf463136e48be471a680f2cf08c90d0bf82be Mon Sep 17 00:00:00 2001 From: bgamard Date: Thu, 9 Nov 2017 13:36:10 +0100 Subject: [PATCH 043/288] Closes #146: no cache --- .../sismics/util/filter/RequestContextFilter.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs-web-common/src/main/java/com/sismics/util/filter/RequestContextFilter.java b/docs-web-common/src/main/java/com/sismics/util/filter/RequestContextFilter.java index b106a4e1..f54b843c 100644 --- a/docs-web-common/src/main/java/com/sismics/util/filter/RequestContextFilter.java +++ b/docs-web-common/src/main/java/com/sismics/util/filter/RequestContextFilter.java @@ -12,6 +12,7 @@ import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.core.HttpHeaders; import com.lowagie.text.FontFactory; import org.apache.log4j.Level; @@ -97,6 +98,7 @@ public class RequestContextFilter implements Filter { tx.begin(); try { + addCacheHeaders(response); filterChain.doFilter(request, response); } catch (Exception e) { ThreadLocalContext.cleanup(); @@ -151,4 +153,15 @@ public class RequestContextFilter implements Filter { ThreadLocalContext.cleanup(); } + + /** + * Add no-cache header. + * + * @param response Response + */ + private void addCacheHeaders(ServletResponse response) { + HttpServletResponse r = (HttpServletResponse) response; + r.addHeader(HttpHeaders.CACHE_CONTROL, "no-cache"); + r.addHeader(HttpHeaders.EXPIRES, "0"); + } } From 36b4fbd303e4cdb843ab59bcf12e9e6d2026abb8 Mon Sep 17 00:00:00 2001 From: bgamard Date: Thu, 9 Nov 2017 13:36:41 +0100 Subject: [PATCH 044/288] ie fix --- .../src/partial/docs/directive.acledit.html | 2 +- .../partial/docs/directive.selectrelation.html | 2 +- .../src/partial/docs/directive.selecttag.html | 2 +- .../webapp/src/partial/docs/document.edit.html | 16 ++++++++-------- .../main/webapp/src/partial/docs/document.html | 2 +- .../webapp/src/partial/docs/document.view.html | 2 +- .../src/main/webapp/src/partial/docs/login.html | 6 +++--- .../src/partial/docs/settings.account.html | 4 ++-- .../webapp/src/partial/docs/settings.config.html | 2 +- .../src/partial/docs/settings.group.edit.html | 6 +++--- .../src/partial/docs/settings.user.edit.html | 10 +++++----- .../src/partial/docs/settings.vocabulary.html | 2 +- .../src/main/webapp/src/partial/docs/tag.html | 4 ++-- .../main/webapp/src/partial/docs/usergroup.html | 4 ++-- 14 files changed, 32 insertions(+), 32 deletions(-) diff --git a/docs-web/src/main/webapp/src/partial/docs/directive.acledit.html b/docs-web/src/main/webapp/src/partial/docs/directive.acledit.html index 11c27e81..44e69bc7 100644 --- a/docs-web/src/main/webapp/src/partial/docs/directive.acledit.html +++ b/docs-web/src/main/webapp/src/partial/docs/directive.acledit.html @@ -26,7 +26,7 @@
    diff --git a/docs-web/src/main/webapp/src/partial/docs/directive.selectrelation.html b/docs-web/src/main/webapp/src/partial/docs/directive.selectrelation.html index 933bb65e..3ffa8bda 100644 --- a/docs-web/src/main/webapp/src/partial/docs/directive.selectrelation.html +++ b/docs-web/src/main/webapp/src/partial/docs/directive.selectrelation.html @@ -6,7 +6,7 @@ -
    \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/partial/docs/directive.selecttag.html b/docs-web/src/main/webapp/src/partial/docs/directive.selecttag.html index 140b8224..9cd24aef 100644 --- a/docs-web/src/main/webapp/src/partial/docs/directive.selecttag.html +++ b/docs-web/src/main/webapp/src/partial/docs/directive.selecttag.html @@ -6,6 +6,6 @@ - \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/partial/docs/document.edit.html b/docs-web/src/main/webapp/src/partial/docs/document.edit.html index dacd109d..70ed9d04 100644 --- a/docs-web/src/main/webapp/src/partial/docs/document.edit.html +++ b/docs-web/src/main/webapp/src/partial/docs/document.edit.html @@ -25,7 +25,7 @@
    @@ -33,7 +33,7 @@
    -
    @@ -75,7 +75,7 @@
    + {{ orphanFiles.length }} file{{ orphanFiles.length > 1 ? 's' : '' }} {{ 'document.edit.orphan_files' | translate: '{ count: orphanFiles.length }' }} -
    + {{newFiles}}
    @@ -95,35 +95,35 @@
    + ng-attr-placeholder="{{ 'document.edit.subject_placeholder' | translate }}" name="subject" ng-model="document.subject" ng-disabled="fileIsUploading" />
    + ng-attr-placeholder="{{ 'document.edit.identifier_placeholder' | translate }}" name="identifier" ng-model="document.identifier" ng-disabled="fileIsUploading" />
    + ng-attr-placeholder="{{ 'document.edit.publisher_placeholder' | translate }}" name="publisher" ng-model="document.publisher" ng-disabled="fileIsUploading" />
    + ng-attr-placeholder="{{ 'document.edit.format_placeholder' | translate }}" name="format" ng-model="document.format" ng-disabled="fileIsUploading" />
    + ng-attr-placeholder="{{ 'document.edit.source_placeholder' | translate }}" name="source" ng-model="document.source" ng-disabled="fileIsUploading" />
    diff --git a/docs-web/src/main/webapp/src/partial/docs/document.html b/docs-web/src/main/webapp/src/partial/docs/document.html index 6cc33f91..09de4487 100644 --- a/docs-web/src/main/webapp/src/partial/docs/document.html +++ b/docs-web/src/main/webapp/src/partial/docs/document.html @@ -29,7 +29,7 @@ lang:fra
    by:user1"> - + diff --git a/docs-web/src/main/webapp/src/partial/docs/document.view.html b/docs-web/src/main/webapp/src/partial/docs/document.view.html index 6af82ba9..2d2205bf 100644 --- a/docs-web/src/main/webapp/src/partial/docs/document.view.html +++ b/docs-web/src/main/webapp/src/partial/docs/document.view.html @@ -117,7 +117,7 @@
    - +
    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 a123313f..054b5854 100644 --- a/docs-web/src/main/webapp/src/partial/docs/login.html +++ b/docs-web/src/main/webapp/src/partial/docs/login.html @@ -17,12 +17,12 @@
    - +
    - +
    @@ -31,7 +31,7 @@
    - +
    diff --git a/docs-web/src/main/webapp/src/partial/docs/settings.account.html b/docs-web/src/main/webapp/src/partial/docs/settings.account.html index 7d2044f0..580283de 100644 --- a/docs-web/src/main/webapp/src/partial/docs/settings.account.html +++ b/docs-web/src/main/webapp/src/partial/docs/settings.account.html @@ -4,7 +4,7 @@
    + ng-minlength="8" ng-maxlength="50" ng-attr-placeholder="{{ 'settings.account.password' | translate }}" ng-model="user.password" />
    {{ 'validation.required' | translate }} @@ -18,7 +18,7 @@
    + ng-attr-placeholder="{{ 'settings.account.password_confirm' | translate }}" ng-model="user.passwordconfirm" />
    {{ 'validation.password_confirm' | translate }} diff --git a/docs-web/src/main/webapp/src/partial/docs/settings.config.html b/docs-web/src/main/webapp/src/partial/docs/settings.config.html index 4a4d6214..60f6ce24 100644 --- a/docs-web/src/main/webapp/src/partial/docs/settings.config.html +++ b/docs-web/src/main/webapp/src/partial/docs/settings.config.html @@ -32,7 +32,7 @@
    -
    diff --git a/docs-web/src/main/webapp/src/partial/docs/settings.group.edit.html b/docs-web/src/main/webapp/src/partial/docs/settings.group.edit.html index eacf1aa6..476f7a64 100644 --- a/docs-web/src/main/webapp/src/partial/docs/settings.group.edit.html +++ b/docs-web/src/main/webapp/src/partial/docs/settings.group.edit.html @@ -9,7 +9,7 @@
    + ng-minlength="3" ng-maxlength="50" ng-attr-placeholder="{{ 'settings.group.edit.name' | translate }}" ng-model="group.name"/>
    @@ -24,7 +24,7 @@
    @@ -49,7 +49,7 @@
    diff --git a/docs-web/src/main/webapp/src/partial/docs/settings.user.edit.html b/docs-web/src/main/webapp/src/partial/docs/settings.user.edit.html index 9482004f..9a0ffedf 100644 --- a/docs-web/src/main/webapp/src/partial/docs/settings.user.edit.html +++ b/docs-web/src/main/webapp/src/partial/docs/settings.user.edit.html @@ -9,7 +9,7 @@
    + ng-minlength="3" ng-maxlength="50" ng-attr-placeholder="{{ 'settings.user.edit.username' | translate }}" ng-model="user.username"/>
    @@ -23,7 +23,7 @@
    + ng-minlength="1" ng-maxlength="100" ng-attr-placeholder="{{ 'settings.user.edit.email' | translate }}" ng-model="user.email"/>
    @@ -49,7 +49,7 @@
    + ng-pattern="/[0-9]*/" ng-attr-placeholder="{{ 'settings.user.edit.storage_quota_placeholder' | translate }}" ng-model="user.storage_quota"/>
    {{ 'filter.filesize.mb' | translate }}
    @@ -64,7 +64,7 @@
    + ng-minlength="8" ng-maxlength="50" ng-attr-placeholder="{{ 'settings.user.edit.password' | translate }}" ng-model="user.password"/>
    @@ -80,7 +80,7 @@
    + ng-attr-placeholder="{{ 'settings.user.edit.password_confirm' | translate }}" ng-model="user.passwordconfirm"/>
    diff --git a/docs-web/src/main/webapp/src/partial/docs/settings.vocabulary.html b/docs-web/src/main/webapp/src/partial/docs/settings.vocabulary.html index 17f0e6a3..1895b65b 100644 --- a/docs-web/src/main/webapp/src/partial/docs/settings.vocabulary.html +++ b/docs-web/src/main/webapp/src/partial/docs/settings.vocabulary.html @@ -23,7 +23,7 @@
    - + diff --git a/docs-web/src/main/webapp/src/partial/docs/tag.html b/docs-web/src/main/webapp/src/partial/docs/tag.html index 8dc9d243..cf6dd575 100644 --- a/docs-web/src/main/webapp/src/partial/docs/tag.html +++ b/docs-web/src/main/webapp/src/partial/docs/tag.html @@ -4,7 +4,7 @@

      - {{ 'add' | translate }}

    @@ -13,7 +13,7 @@

    - +

    diff --git a/docs-web/src/main/webapp/src/partial/docs/usergroup.html b/docs-web/src/main/webapp/src/partial/docs/usergroup.html index 7d0865f6..a5b26d7a 100644 --- a/docs-web/src/main/webapp/src/partial/docs/usergroup.html +++ b/docs-web/src/main/webapp/src/partial/docs/usergroup.html @@ -3,7 +3,7 @@

    - +

    @@ -21,7 +21,7 @@

    - +

    From 2957034286dfe1e51ab045daa436b45427b932a5 Mon Sep 17 00:00:00 2001 From: bgamard Date: Thu, 9 Nov 2017 14:39:25 +0100 Subject: [PATCH 045/288] Closes #147: fix IE file upload --- .../docs/controller/document/DocumentEdit.js | 2 +- .../controller/settings/SettingsConfig.js | 2 +- .../webapp/src/app/docs/directive/File.js | 20 ------------------- docs-web/src/main/webapp/src/index.html | 1 - .../src/partial/docs/document.edit.html | 4 ++-- .../partial/docs/document.view.content.html | 17 ++++++++-------- .../src/partial/docs/settings.config.html | 6 +++--- docs-web/src/main/webapp/src/style/main.less | 1 + 8 files changed, 16 insertions(+), 37 deletions(-) delete mode 100644 docs-web/src/main/webapp/src/app/docs/directive/File.js diff --git a/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentEdit.js b/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentEdit.js index a7ec2e48..65e19ea5 100644 --- a/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentEdit.js +++ b/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentEdit.js @@ -148,7 +148,7 @@ angular.module('docs').controller('DocumentEdit', function($rootScope, $scope, $ var file = $scope.newFiles[key]; var formData = new FormData(); formData.append('id', data.id); - formData.append('file', file); + formData.append('file', file, file.name); // Send the file $.ajax({ diff --git a/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsConfig.js b/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsConfig.js index 2c533851..5e0532d5 100644 --- a/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsConfig.js +++ b/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsConfig.js @@ -39,7 +39,7 @@ angular.module('docs').controller('SettingsConfig', function($scope, $rootScope, $scope.sendImage = function(type, image) { // Build the payload var formData = new FormData(); - formData.append('image', image); + formData.append('image', image[0]); // Send the file var done = function() { diff --git a/docs-web/src/main/webapp/src/app/docs/directive/File.js b/docs-web/src/main/webapp/src/app/docs/directive/File.js deleted file mode 100644 index 9e0b0be7..00000000 --- a/docs-web/src/main/webapp/src/app/docs/directive/File.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict'; - -/** - * File upload directive. - */ -angular.module('docs').directive('file', function() { - return { - restrict: 'E', - template: '', - replace: true, - require: 'ngModel', - link: function(scope, element, attrs, ctrl) { - element.bind('change', function() { - scope.$apply(function() { - attrs.multiple ? ctrl.$setViewValue(element[0].files) : ctrl.$setViewValue(element[0].files[0]); - }); - }); - } - } -}); \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/index.html b/docs-web/src/main/webapp/src/index.html index 42c0ae2f..82c7d25f 100644 --- a/docs-web/src/main/webapp/src/index.html +++ b/docs-web/src/main/webapp/src/index.html @@ -79,7 +79,6 @@ - diff --git a/docs-web/src/main/webapp/src/partial/docs/document.edit.html b/docs-web/src/main/webapp/src/partial/docs/document.edit.html index 70ed9d04..a450cdff 100644 --- a/docs-web/src/main/webapp/src/partial/docs/document.edit.html +++ b/docs-web/src/main/webapp/src/partial/docs/document.edit.html @@ -70,12 +70,12 @@
    - +
    + {{ orphanFiles.length }} file{{ orphanFiles.length > 1 ? 's' : '' }} {{ 'document.edit.orphan_files' | translate: '{ count: orphanFiles.length }' }} -
    {{newFiles}} +
    diff --git a/docs-web/src/main/webapp/src/partial/docs/document.view.content.html b/docs-web/src/main/webapp/src/partial/docs/document.view.content.html index c7afc320..5821da00 100644 --- a/docs-web/src/main/webapp/src/partial/docs/document.view.content.html +++ b/docs-web/src/main/webapp/src/partial/docs/document.view.content.html @@ -72,16 +72,15 @@ {{ 'document.view.content.drop_zone' | translate }}

    +
    -
    -
    - -
    +
    +
    diff --git a/docs-web/src/main/webapp/src/partial/docs/settings.config.html b/docs-web/src/main/webapp/src/partial/docs/settings.config.html index 60f6ce24..fac842b1 100644 --- a/docs-web/src/main/webapp/src/partial/docs/settings.config.html +++ b/docs-web/src/main/webapp/src/partial/docs/settings.config.html @@ -40,7 +40,7 @@
    -
    @@ -54,8 +54,8 @@
    -
    form = {{user | json}}
    master = {{master | json}}
    + + + // Module: copyExample + angular. + module('copyExample', []). + controller('ExampleController', ['$scope', function($scope) { + $scope.master = {}; - - - + $scope.reset(); + }]); + + */ - function copy(source, destination){ - if (isWindow(source) || isScope(source)) { - throw ngMinErr('cpws', - "Can't copy! Making copies of Window or Scope instances is not supported."); + function copy(source, destination, maxDepth) { + var stackSource = []; + var stackDest = []; + maxDepth = isValidObjectMaxDepth(maxDepth) ? maxDepth : NaN; + + if (destination) { + if (isTypedArray(destination) || isArrayBuffer(destination)) { + throw ngMinErr('cpta', 'Can\'t copy! TypedArray destination cannot be mutated.'); + } + if (source === destination) { + throw ngMinErr('cpi', 'Can\'t copy! Source and destination are identical.'); + } + + // Empty the destination object + if (isArray(destination)) { + destination.length = 0; + } else { + forEach(destination, function(value, key) { + if (key !== '$$hashKey') { + delete destination[key]; + } + }); + } + + stackSource.push(source); + stackDest.push(destination); + return copyRecurse(source, destination, maxDepth); } - if (!destination) { - destination = source; - if (source) { - if (isArray(source)) { - destination = copy(source, []); - } else if (isDate(source)) { - destination = new Date(source.getTime()); - } else if (isRegExp(source)) { - destination = new RegExp(source.source); - } else if (isObject(source)) { - destination = copy(source, {}); - } + return copyElement(source, maxDepth); + + function copyRecurse(source, destination, maxDepth) { + maxDepth--; + if (maxDepth < 0) { + return '...'; } - } else { - if (source === destination) throw ngMinErr('cpi', - "Can't copy! Source and destination are identical."); + var h = destination.$$hashKey; + var key; if (isArray(source)) { - destination.length = 0; - for ( var i = 0; i < source.length; i++) { - destination.push(copy(source[i])); + for (var i = 0, ii = source.length; i < ii; i++) { + destination.push(copyElement(source[i], maxDepth)); + } + } else if (isBlankObject(source)) { + // createMap() fast path --- Safe to avoid hasOwnProperty check because prototype chain is empty + for (key in source) { + destination[key] = copyElement(source[key], maxDepth); + } + } else if (source && typeof source.hasOwnProperty === 'function') { + // Slow path, which must rely on hasOwnProperty + for (key in source) { + if (source.hasOwnProperty(key)) { + destination[key] = copyElement(source[key], maxDepth); + } } } else { - var h = destination.$$hashKey; - forEach(destination, function(value, key){ - delete destination[key]; - }); - for ( var key in source) { - destination[key] = copy(source[key]); + // Slowest path --- hasOwnProperty can't be called as a method + for (key in source) { + if (hasOwnProperty.call(source, key)) { + destination[key] = copyElement(source[key], maxDepth); + } } - setHashKey(destination,h); - } - } - return destination; - } - - /** - * Create a shallow copy of an object - */ - function shallowCopy(src, dst) { - dst = dst || {}; - - for(var key in src) { - // shallowCopy is only ever called by $compile nodeLinkFn, which has control over src - // so we don't need to worry about using our custom hasOwnProperty here - if (src.hasOwnProperty(key) && key.charAt(0) !== '$' && key.charAt(1) !== '$') { - dst[key] = src[key]; } + setHashKey(destination, h); + return destination; } - return dst; + function copyElement(source, maxDepth) { + // Simple values + if (!isObject(source)) { + return source; + } + + // Already copied values + var index = stackSource.indexOf(source); + if (index !== -1) { + return stackDest[index]; + } + + if (isWindow(source) || isScope(source)) { + throw ngMinErr('cpws', + 'Can\'t copy! Making copies of Window or Scope instances is not supported.'); + } + + var needsRecurse = false; + var destination = copyType(source); + + if (destination === undefined) { + destination = isArray(source) ? [] : Object.create(getPrototypeOf(source)); + needsRecurse = true; + } + + stackSource.push(source); + stackDest.push(destination); + + return needsRecurse + ? copyRecurse(source, destination, maxDepth) + : destination; + } + + function copyType(source) { + switch (toString.call(source)) { + case '[object Int8Array]': + case '[object Int16Array]': + case '[object Int32Array]': + case '[object Float32Array]': + case '[object Float64Array]': + case '[object Uint8Array]': + case '[object Uint8ClampedArray]': + case '[object Uint16Array]': + case '[object Uint32Array]': + return new source.constructor(copyElement(source.buffer), source.byteOffset, source.length); + + case '[object ArrayBuffer]': + // Support: IE10 + if (!source.slice) { + // If we're in this case we know the environment supports ArrayBuffer + /* eslint-disable no-undef */ + var copied = new ArrayBuffer(source.byteLength); + new Uint8Array(copied).set(new Uint8Array(source)); + /* eslint-enable */ + return copied; + } + return source.slice(0); + + case '[object Boolean]': + case '[object Number]': + case '[object String]': + case '[object Date]': + return new source.constructor(source.valueOf()); + + case '[object RegExp]': + var re = new RegExp(source.source, source.toString().match(/[^/]*$/)[0]); + re.lastIndex = source.lastIndex; + return re; + + case '[object Blob]': + return new source.constructor([source], {type: source.type}); + } + + if (isFunction(source.cloneNode)) { + return source.cloneNode(true); + } + } } +// eslint-disable-next-line no-self-compare + function simpleCompare(a, b) { return a === b || (a !== a && b !== b); } + + /** * @ngdoc function * @name angular.equals - * @function + * @module ng + * @kind function * * @description * Determines if two objects or two values are equivalent. Supports value types, regular @@ -874,7 +1167,7 @@ * * Both objects or values are of the same type and all of their properties are equal by * comparing them with `angular.equals`. * * Both values are NaN. (In JavaScript, NaN == NaN => false. But we consider two NaN as equal) - * * Both values represent the same regular expression (In JavasScript, + * * Both values represent the same regular expression (In JavaScript, * /abc/ == /abc/ => false. But we consider two regular expressions as equal when their textual * representation matches). * @@ -886,54 +1179,171 @@ * @param {*} o1 Object or value to compare. * @param {*} o2 Object or value to compare. * @returns {boolean} True if arguments are equal. + * + * @example + + +
    +
    +

    User 1

    + Name: + Age: + +

    User 2

    + Name: + Age: + +
    +
    + +
    + User 1:
    {{user1 | json}}
    + User 2:
    {{user2 | json}}
    + Equal:
    {{result}}
    + +
    +
    + + angular.module('equalsExample', []).controller('ExampleController', ['$scope', function($scope) { + $scope.user1 = {}; + $scope.user2 = {}; + $scope.compare = function() { + $scope.result = angular.equals($scope.user1, $scope.user2); + }; + }]); + +
    */ function equals(o1, o2) { if (o1 === o2) return true; if (o1 === null || o2 === null) return false; + // eslint-disable-next-line no-self-compare if (o1 !== o1 && o2 !== o2) return true; // NaN === NaN var t1 = typeof o1, t2 = typeof o2, length, key, keySet; - if (t1 == t2) { - if (t1 == 'object') { - if (isArray(o1)) { - if (!isArray(o2)) return false; - if ((length = o1.length) == o2.length) { - for(key=0; key + + ... + ... + + ``` + * @example + * This example shows how to use a jQuery based library of a different name. + * The library name must be available at the top most 'window'. + ```html + + + ... + ... + + ``` + */ + var jq = function() { + if (isDefined(jq.name_)) return jq.name_; + var el; + var i, ii = ngAttrPrefixes.length, prefix, name; + for (i = 0; i < ii; ++i) { + prefix = ngAttrPrefixes[i]; + el = window.document.querySelector('[' + prefix.replace(':', '\\:') + 'jq]'); + if (el) { + name = el.getAttribute(prefix + 'jq'); + break; + } + } + + return (jq.name_ = name); + }; function concat(array1, array2, index) { return array1.concat(slice.call(array2, index)); @@ -944,11 +1354,11 @@ } - /* jshint -W101 */ /** * @ngdoc function * @name angular.bind - * @function + * @module ng + * @kind function * * @description * Returns a function which calls function `fn` bound to `self` (`self` becomes the `this` for @@ -961,23 +1371,22 @@ * @param {...*} args Optional arguments to be prebound to the `fn` function call. * @returns {function()} Function that wraps the `fn` with all the specified bindings. */ - /* jshint +W101 */ function bind(self, fn) { var curryArgs = arguments.length > 2 ? sliceArgs(arguments, 2) : []; if (isFunction(fn) && !(fn instanceof RegExp)) { return curryArgs.length - ? function() { - return arguments.length - ? fn.apply(self, curryArgs.concat(slice.call(arguments, 0))) + ? function() { + return arguments.length + ? fn.apply(self, concat(curryArgs, arguments, 0)) : fn.apply(self, curryArgs); - } - : function() { - return arguments.length + } + : function() { + return arguments.length ? fn.apply(self, arguments) : fn.call(self); - }; + }; } else { - // in IE, native methods are not functions so they cannot be bound (note: they don't need to be) + // In IE, native methods are not functions so they cannot be bound (note: they don't need to be). return fn; } } @@ -986,11 +1395,11 @@ function toJsonReplacer(key, value) { var val = value; - if (typeof key === 'string' && key.charAt(0) === '$') { + if (typeof key === 'string' && key.charAt(0) === '$' && key.charAt(1) === '$') { val = undefined; } else if (isWindow(value)) { val = '$WINDOW'; - } else if (value && document === value) { + } else if (value && window.document === value) { val = '$DOCUMENT'; } else if (isScope(value)) { val = '$SCOPE'; @@ -1003,71 +1412,104 @@ /** * @ngdoc function * @name angular.toJson - * @function + * @module ng + * @kind function * * @description - * Serializes input into a JSON-formatted string. Properties with leading $ characters will be + * Serializes input into a JSON-formatted string. Properties with leading $$ characters will be * stripped since angular uses this notation internally. * - * @param {Object|Array|Date|string|number} obj Input to be serialized into JSON. - * @param {boolean=} pretty If set to true, the JSON output will contain newlines and whitespace. + * @param {Object|Array|Date|string|number|boolean} obj Input to be serialized into JSON. + * @param {boolean|number} [pretty=2] If set to true, the JSON output will contain newlines and whitespace. + * If set to an integer, the JSON output will contain that many spaces per indentation. * @returns {string|undefined} JSON-ified string representing `obj`. + * @knownIssue + * + * The Safari browser throws a `RangeError` instead of returning `null` when it tries to stringify a `Date` + * object with an invalid date value. The only reliable way to prevent this is to monkeypatch the + * `Date.prototype.toJSON` method as follows: + * + * ``` + * var _DatetoJSON = Date.prototype.toJSON; + * Date.prototype.toJSON = function() { + * try { + * return _DatetoJSON.call(this); + * } catch(e) { + * if (e instanceof RangeError) { + * return null; + * } + * throw e; + * } + * }; + * ``` + * + * See https://github.com/angular/angular.js/pull/14221 for more information. */ function toJson(obj, pretty) { - if (typeof obj === 'undefined') return undefined; - return JSON.stringify(obj, toJsonReplacer, pretty ? ' ' : null); + if (isUndefined(obj)) return undefined; + if (!isNumber(pretty)) { + pretty = pretty ? 2 : null; + } + return JSON.stringify(obj, toJsonReplacer, pretty); } /** * @ngdoc function * @name angular.fromJson - * @function + * @module ng + * @kind function * * @description * Deserializes a JSON string. * * @param {string} json JSON string to deserialize. - * @returns {Object|Array|Date|string|number} Deserialized thingy. + * @returns {Object|Array|string|number} Deserialized JSON string. */ function fromJson(json) { return isString(json) - ? JSON.parse(json) - : json; + ? JSON.parse(json) + : json; } - function toBoolean(value) { - if (typeof value === 'function') { - value = true; - } else if (value && value.length !== 0) { - var v = lowercase("" + value); - value = !(v == 'f' || v == '0' || v == 'false' || v == 'no' || v == 'n' || v == '[]'); - } else { - value = false; - } - return value; + var ALL_COLONS = /:/g; + function timezoneToOffset(timezone, fallback) { + // Support: IE 9-11 only, Edge 13-15+ + // IE/Edge do not "understand" colon (`:`) in timezone + timezone = timezone.replace(ALL_COLONS, ''); + var requestedTimezoneOffset = Date.parse('Jan 01, 1970 00:00:00 ' + timezone) / 60000; + return isNumberNaN(requestedTimezoneOffset) ? fallback : requestedTimezoneOffset; } + + function addDateMinutes(date, minutes) { + date = new Date(date.getTime()); + date.setMinutes(date.getMinutes() + minutes); + return date; + } + + + function convertTimezoneToLocal(date, timezone, reverse) { + reverse = reverse ? -1 : 1; + var dateTimezoneOffset = date.getTimezoneOffset(); + var timezoneOffset = timezoneToOffset(timezone, dateTimezoneOffset); + return addDateMinutes(date, reverse * (timezoneOffset - dateTimezoneOffset)); + } + + /** * @returns {string} Returns the string representation of the element. */ function startingTag(element) { - element = jqLite(element).clone(); - try { - // turns out IE does not let you set .html() on elements which - // are not allowed to have children. So we just ignore it. - element.empty(); - } catch(e) {} - // As Per DOM Standards - var TEXT_NODE = 3; + element = jqLite(element).clone().empty(); var elemHtml = jqLite('
    ').append(element).html(); try { - return element[0].nodeType === TEXT_NODE ? lowercase(elemHtml) : - elemHtml. - match(/^(<[^>]+>)/)[1]. - replace(/^<([\w\-]+)/, function(match, nodeName) { return '<' + lowercase(nodeName); }); - } catch(e) { + return element[0].nodeType === NODE_TYPE_TEXT ? lowercase(elemHtml) : + elemHtml. + match(/^(<[^>]+>)/)[1]. + replace(/^<([\w-]+)/, function(match, nodeName) {return '<' + lowercase(nodeName);}); + } catch (e) { return lowercase(elemHtml); } @@ -1087,27 +1529,33 @@ function tryDecodeURIComponent(value) { try { return decodeURIComponent(value); - } catch(e) { - // Ignore any invalid uri component + } catch (e) { + // Ignore any invalid uri component. } } /** * Parses an escaped url query string into key-value pairs. - * @returns Object.<(string|boolean)> + * @returns {Object.} */ function parseKeyValue(/**string*/keyValue) { - var obj = {}, key_value, key; - forEach((keyValue || "").split('&'), function(keyValue){ - if ( keyValue ) { - key_value = keyValue.split('='); - key = tryDecodeURIComponent(key_value[0]); - if ( isDefined(key) ) { - var val = isDefined(key_value[1]) ? tryDecodeURIComponent(key_value[1]) : true; - if (!obj[key]) { + var obj = {}; + forEach((keyValue || '').split('&'), function(keyValue) { + var splitPoint, key, val; + if (keyValue) { + key = keyValue = keyValue.replace(/\+/g,'%20'); + splitPoint = keyValue.indexOf('='); + if (splitPoint !== -1) { + key = keyValue.substring(0, splitPoint); + val = keyValue.substring(splitPoint + 1); + } + key = tryDecodeURIComponent(key); + if (isDefined(key)) { + val = isDefined(val) ? tryDecodeURIComponent(val) : true; + if (!hasOwnProperty.call(obj, key)) { obj[key] = val; - } else if(isArray(obj[key])) { + } else if (isArray(obj[key])) { obj[key].push(val); } else { obj[key] = [obj[key],val]; @@ -1124,11 +1572,11 @@ if (isArray(value)) { forEach(value, function(arrayValue) { parts.push(encodeUriQuery(key, true) + - (arrayValue === true ? '' : '=' + encodeUriQuery(arrayValue, true))); + (arrayValue === true ? '' : '=' + encodeUriQuery(arrayValue, true))); }); } else { parts.push(encodeUriQuery(key, true) + - (value === true ? '' : '=' + encodeUriQuery(value, true))); + (value === true ? '' : '=' + encodeUriQuery(value, true))); } }); return parts.length ? parts.join('&') : ''; @@ -1148,9 +1596,9 @@ */ function encodeUriSegment(val) { return encodeUriQuery(val, true). - replace(/%26/gi, '&'). - replace(/%3D/gi, '='). - replace(/%2B/gi, '+'); + replace(/%26/gi, '&'). + replace(/%3D/gi, '='). + replace(/%2B/gi, '+'); } @@ -1158,7 +1606,7 @@ * This method is intended for encoding *key* or *value* parts of query component. We need a custom * method because encodeURIComponent is too aggressive and encodes stuff that doesn't have to be * encoded per http://tools.ietf.org/html/rfc3986: - * query = *( pchar / "/" / "?" ) + * query = *( pchar / "/" / "?" ) * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" * pct-encoded = "%" HEXDIG HEXDIG @@ -1167,21 +1615,92 @@ */ function encodeUriQuery(val, pctEncodeSpaces) { return encodeURIComponent(val). - replace(/%40/gi, '@'). - replace(/%3A/gi, ':'). - replace(/%24/g, '$'). - replace(/%2C/gi, ','). - replace(/%20/g, (pctEncodeSpaces ? '%20' : '+')); + replace(/%40/gi, '@'). + replace(/%3A/gi, ':'). + replace(/%24/g, '$'). + replace(/%2C/gi, ','). + replace(/%3B/gi, ';'). + replace(/%20/g, (pctEncodeSpaces ? '%20' : '+')); } + var ngAttrPrefixes = ['ng-', 'data-ng-', 'ng:', 'x-ng-']; + + function getNgAttribute(element, ngAttr) { + var attr, i, ii = ngAttrPrefixes.length; + for (i = 0; i < ii; ++i) { + attr = ngAttrPrefixes[i] + ngAttr; + if (isString(attr = element.getAttribute(attr))) { + return attr; + } + } + return null; + } + + function allowAutoBootstrap(document) { + var script = document.currentScript; + + if (!script) { + // Support: IE 9-11 only + // IE does not have `document.currentScript` + return true; + } + + // If the `currentScript` property has been clobbered just return false, since this indicates a probable attack + if (!(script instanceof window.HTMLScriptElement || script instanceof window.SVGScriptElement)) { + return false; + } + + var attributes = script.attributes; + var srcs = [attributes.getNamedItem('src'), attributes.getNamedItem('href'), attributes.getNamedItem('xlink:href')]; + + return srcs.every(function(src) { + if (!src) { + return true; + } + if (!src.value) { + return false; + } + + var link = document.createElement('a'); + link.href = src.value; + + if (document.location.origin === link.origin) { + // Same-origin resources are always allowed, even for non-whitelisted schemes. + return true; + } + // Disabled bootstrapping unless angular.js was loaded from a known scheme used on the web. + // This is to prevent angular.js bundled with browser extensions from being used to bypass the + // content security policy in web pages and other browser extensions. + switch (link.protocol) { + case 'http:': + case 'https:': + case 'ftp:': + case 'blob:': + case 'file:': + case 'data:': + return true; + default: + return false; + } + }); + } + +// Cached as it has to run during loading so that document.currentScript is available. + var isAutoBootstrapAllowed = allowAutoBootstrap(window.document); /** * @ngdoc directive - * @name ng.directive:ngApp + * @name ngApp + * @module ng * * @element ANY * @param {angular.Module} ngApp an optional application * {@link angular.module module} name to load. + * @param {boolean=} ngStrictDi if this attribute is present on the app element, the injector will be + * created in "strict-di" mode. This means that the application will fail to invoke functions which + * do not use explicit function annotation (and are thus unsuitable for minification), as described + * in {@link guide/di the Dependency Injection guide}, and useful debugging info will assist in + * tracking down the root of these bugs. * * @description * @@ -1189,13 +1708,20 @@ * designates the **root element** of the application and is typically placed near the root element * of the page - e.g. on the `` or `` tags. * - * Only one AngularJS application can be auto-bootstrapped per HTML document. The first `ngApp` - * found in the document will be used to define the root element to auto-bootstrap as an - * application. To run multiple applications in an HTML document you must manually bootstrap them using - * {@link angular.bootstrap} instead. AngularJS applications cannot be nested within each other. + * There are a few things to keep in mind when using `ngApp`: + * - only one AngularJS application can be auto-bootstrapped per HTML document. The first `ngApp` + * found in the document will be used to define the root element to auto-bootstrap as an + * application. To run multiple applications in an HTML document you must manually bootstrap them using + * {@link angular.bootstrap} instead. + * - AngularJS applications cannot be nested within each other. + * - Do not use a directive that uses {@link ng.$compile#transclusion transclusion} on the same element as `ngApp`. + * This includes directives such as {@link ng.ngIf `ngIf`}, {@link ng.ngInclude `ngInclude`} and + * {@link ngRoute.ngView `ngView`}. + * Doing this misplaces the app {@link ng.$rootElement `$rootElement`} and the app's {@link auto.$injector injector}, + * causing animations to stop working and making the injector inaccessible from outside the app. * * You can specify an **AngularJS module** to be used as the root module for the application. This - * module will be loaded into the {@link AUTO.$injector} when the application is bootstrapped and + * module will be loaded into the {@link auto.$injector} when the application is bootstrapped. It * should contain the application code needed or have dependencies on other modules that will * contain the code. See {@link angular.module} for more information. * @@ -1203,12 +1729,13 @@ * document would not be compiled, the `AppController` would not be instantiated and the `{{ a+b }}` * would not be resolved to `3`. * - * `ngApp` is the easiest, and most common, way to bootstrap an application. + * `ngApp` is the easiest, and most common way to bootstrap an application. * - +
    I can add: {{a}} + {{b}} = {{ a+b }} +
    angular.module('ngAppDemo', []).controller('ngAppDemoController', function($scope) { @@ -1218,86 +1745,210 @@
    * + * Using `ngStrictDi`, you would see something like this: + * + + +
    +
    + I can add: {{a}} + {{b}} = {{ a+b }} + +

    This renders because the controller does not fail to + instantiate, by using explicit annotation style (see + script.js for details) +

    +
    + +
    + Name:
    + Hello, {{name}}! + +

    This renders because the controller does not fail to + instantiate, by using explicit annotation style + (see script.js for details) +

    +
    + +
    + I can add: {{a}} + {{b}} = {{ a+b }} + +

    The controller could not be instantiated, due to relying + on automatic function annotations (which are disabled in + strict mode). As such, the content of this section is not + interpolated, and there should be an error in your web console. +

    +
    +
    +
    + + angular.module('ngAppStrictDemo', []) + // BadController will fail to instantiate, due to relying on automatic function annotation, + // rather than an explicit annotation + .controller('BadController', function($scope) { + $scope.a = 1; + $scope.b = 2; + }) + // Unlike BadController, GoodController1 and GoodController2 will not fail to be instantiated, + // due to using explicit annotations using the array style and $inject property, respectively. + .controller('GoodController1', ['$scope', function($scope) { + $scope.a = 1; + $scope.b = 2; + }]) + .controller('GoodController2', GoodController2); + function GoodController2($scope) { + $scope.name = 'World'; + } + GoodController2.$inject = ['$scope']; + + + div[ng-controller] { + margin-bottom: 1em; + -webkit-border-radius: 4px; + border-radius: 4px; + border: 1px solid; + padding: .5em; + } + div[ng-controller^=Good] { + border-color: #d6e9c6; + background-color: #dff0d8; + color: #3c763d; + } + div[ng-controller^=Bad] { + border-color: #ebccd1; + background-color: #f2dede; + color: #a94442; + margin-bottom: 0; + } + +
    */ function angularInit(element, bootstrap) { - var elements = [element], - appElement, - module, - names = ['ng:app', 'ng-app', 'x-ng-app', 'data-ng-app'], - NG_APP_CLASS_REGEXP = /\sng[:\-]app(:\s*([\w\d_]+);?)?\s/; + var appElement, + module, + config = {}; - function append(element) { - element && elements.push(element); - } + // The element `element` has priority over any other element. + forEach(ngAttrPrefixes, function(prefix) { + var name = prefix + 'app'; - forEach(names, function(name) { - names[name] = true; - append(document.getElementById(name)); - name = name.replace(':', '\\:'); - if (element.querySelectorAll) { - forEach(element.querySelectorAll('.' + name), append); - forEach(element.querySelectorAll('.' + name + '\\:'), append); - forEach(element.querySelectorAll('[' + name + ']'), append); + if (!appElement && element.hasAttribute && element.hasAttribute(name)) { + appElement = element; + module = element.getAttribute(name); } }); + forEach(ngAttrPrefixes, function(prefix) { + var name = prefix + 'app'; + var candidate; - forEach(elements, function(element) { - if (!appElement) { - var className = ' ' + element.className + ' '; - var match = NG_APP_CLASS_REGEXP.exec(className); - if (match) { - appElement = element; - module = (match[2] || '').replace(/\s+/g, ','); - } else { - forEach(element.attributes, function(attr) { - if (!appElement && names[attr.name]) { - appElement = element; - module = attr.value; - } - }); - } + if (!appElement && (candidate = element.querySelector('[' + name.replace(':', '\\:') + ']'))) { + appElement = candidate; + module = candidate.getAttribute(name); } }); if (appElement) { - bootstrap(appElement, module ? [module] : []); + if (!isAutoBootstrapAllowed) { + window.console.error('Angular: disabling automatic bootstrap. + * + * + * + * ``` + * + * @param {DOMElement} element DOM element which is the root of angular application. * @param {Array=} modules an array of modules to load into the application. * Each item in the array should be the name of a predefined module or a (DI annotated) - * function that will be invoked by the injector as a run block. + * function that will be invoked by the injector as a `config` block. * See: {@link angular.module modules} - * @returns {AUTO.$injector} Returns the newly created injector for this app. + * @param {Object=} config an object for defining configuration options for the application. The + * following keys are supported: + * + * * `strictDi` - disable automatic function annotation for the application. This is meant to + * assist in finding bugs which break minified code. Defaults to `false`. + * + * @returns {auto.$injector} Returns the newly created injector for this app. */ - function bootstrap(element, modules) { + function bootstrap(element, modules, config) { + if (!isObject(config)) config = {}; + var defaultConfig = { + strictDi: false + }; + config = extend(defaultConfig, config); var doBootstrap = function() { element = jqLite(element); if (element.injector()) { - var tag = (element[0] === document) ? 'document' : startingTag(element); - throw ngMinErr('btstrpd', "App Already Bootstrapped with this Element '{0}'", tag); + var tag = (element[0] === window.document) ? 'document' : startingTag(element); + // Encode angle brackets to prevent input from being sanitized to empty string #8683. + throw ngMinErr( + 'btstrpd', + 'App already bootstrapped with this element \'{0}\'', + tag.replace(//,'>')); } modules = modules || []; modules.unshift(['$provide', function($provide) { $provide.value('$rootElement', element); }]); + + if (config.debugInfoEnabled) { + // Pushing so that this overrides `debugInfoEnabled` setting defined in user's `modules`. + modules.push(['$compileProvider', function($compileProvider) { + $compileProvider.debugInfoEnabled(true); + }]); + } + modules.unshift('ng'); - var injector = createInjector(modules); - injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector', '$animate', - function(scope, element, compile, injector, animate) { + var injector = createInjector(modules, config.strictDi); + injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector', + function bootstrapApply(scope, element, compile, injector) { scope.$apply(function() { element.data('$injector', injector); compile(element)(scope); @@ -1307,8 +1958,14 @@ return injector; }; + var NG_ENABLE_DEBUG_INFO = /^NG_ENABLE_DEBUG_INFO!/; var NG_DEFER_BOOTSTRAP = /^NG_DEFER_BOOTSTRAP!/; + if (window && NG_ENABLE_DEBUG_INFO.test(window.name)) { + config.debugInfoEnabled = true; + window.name = window.name.replace(NG_ENABLE_DEBUG_INFO, ''); + } + if (window && !NG_DEFER_BOOTSTRAP.test(window.name)) { return doBootstrap(); } @@ -1318,40 +1975,104 @@ forEach(extraModules, function(module) { modules.push(module); }); - doBootstrap(); + return doBootstrap(); }; + + if (isFunction(angular.resumeDeferredBootstrap)) { + angular.resumeDeferredBootstrap(); + } + } + + /** + * @ngdoc function + * @name angular.reloadWithDebugInfo + * @module ng + * @description + * Use this function to reload the current application with debug information turned on. + * This takes precedence over a call to `$compileProvider.debugInfoEnabled(false)`. + * + * See {@link ng.$compileProvider#debugInfoEnabled} for more. + */ + function reloadWithDebugInfo() { + window.name = 'NG_ENABLE_DEBUG_INFO!' + window.name; + window.location.reload(); + } + + /** + * @name angular.getTestability + * @module ng + * @description + * Get the testability service for the instance of Angular on the given + * element. + * @param {DOMElement} element DOM element which is the root of angular application. + */ + function getTestability(rootElement) { + var injector = angular.element(rootElement).injector(); + if (!injector) { + throw ngMinErr('test', + 'no injector found for element argument to getTestability'); + } + return injector.get('$$testability'); } var SNAKE_CASE_REGEXP = /[A-Z]/g; - function snake_case(name, separator){ + function snake_case(name, separator) { separator = separator || '_'; return name.replace(SNAKE_CASE_REGEXP, function(letter, pos) { return (pos ? separator : '') + letter.toLowerCase(); }); } + var bindJQueryFired = false; function bindJQuery() { + var originalCleanData; + + if (bindJQueryFired) { + return; + } + // bind to jQuery if present; - jQuery = window.jQuery; - // reset to jQuery or default to us. - if (jQuery) { + var jqName = jq(); + jQuery = isUndefined(jqName) ? window.jQuery : // use jQuery (if present) + !jqName ? undefined : // use jqLite + window[jqName]; // use jQuery specified by `ngJq` + + // Use jQuery if it exists with proper functionality, otherwise default to us. + // Angular 1.2+ requires jQuery 1.7+ for on()/off() support. + // Angular 1.3+ technically requires at least jQuery 2.1+ but it may work with older + // versions. It will not work for sure with jQuery <1.7, though. + if (jQuery && jQuery.fn.on) { jqLite = jQuery; extend(jQuery.fn, { scope: JQLitePrototype.scope, isolateScope: JQLitePrototype.isolateScope, - controller: JQLitePrototype.controller, + controller: /** @type {?} */ (JQLitePrototype).controller, injector: JQLitePrototype.injector, inheritedData: JQLitePrototype.inheritedData }); - // Method signature: - // jqLitePatchJQueryRemove(name, dispatchThis, filterElems, getterIfNoArguments) - jqLitePatchJQueryRemove('remove', true, true, false); - jqLitePatchJQueryRemove('empty', false, false, false); - jqLitePatchJQueryRemove('html', false, false, true); + + // All nodes removed from the DOM via various jQuery APIs like .remove() + // are passed through jQuery.cleanData. Monkey-patch this method to fire + // the $destroy event on all removed nodes. + originalCleanData = jQuery.cleanData; + jQuery.cleanData = function(elems) { + var events; + for (var i = 0, elem; (elem = elems[i]) != null; i++) { + events = jQuery._data(elem, 'events'); + if (events && events.$destroy) { + jQuery(elem).triggerHandler('$destroy'); + } + } + originalCleanData(elems); + }; } else { jqLite = JQLite; } + angular.element = jqLite; + + // Prevent double-proxying. + bindJQueryFired = true; } /** @@ -1359,7 +2080,7 @@ */ function assertArg(arg, name, reason) { if (!arg) { - throw ngMinErr('areq', "Argument '{0}' is {1}", (name || '?'), (reason || "required")); + throw ngMinErr('areq', 'Argument \'{0}\' is {1}', (name || '?'), (reason || 'required')); } return arg; } @@ -1370,7 +2091,7 @@ } assertArg(isFunction(arg), name, 'not a function, got ' + - (arg && typeof arg == 'object' ? arg.constructor.name || 'Object' : typeof arg)); + (arg && typeof arg === 'object' ? arg.constructor.name || 'Object' : typeof arg)); return arg; } @@ -1381,16 +2102,16 @@ */ function assertNotHasOwnProperty(name, context) { if (name === 'hasOwnProperty') { - throw ngMinErr('badname', "hasOwnProperty is not a valid {0} name", context); + throw ngMinErr('badname', 'hasOwnProperty is not a valid {0} name', context); } } /** * Return the value accessible from the object by path. Any undefined traversals are ignored * @param {Object} obj starting object - * @param {string} path path to traverse - * @param {boolean=true} bindFnToScope - * @returns value as accessible by path + * @param {String} path path to traverse + * @param {boolean} [bindFnToScope=true] + * @returns {Object} value as accessible by path */ //TODO(misko): this function needs to be removed function getter(obj, path, bindFnToScope) { @@ -1415,30 +2136,74 @@ /** * Return the DOM siblings between the first and last node in the given array. * @param {Array} array like object - * @returns jQlite object containing the elements + * @returns {Array} the inputted object or a jqLite collection containing the nodes */ - function getBlockElements(nodes) { - var startNode = nodes[0], - endNode = nodes[nodes.length - 1]; - if (startNode === endNode) { - return jqLite(startNode); + function getBlockNodes(nodes) { + // TODO(perf): update `nodes` instead of creating a new object? + var node = nodes[0]; + var endNode = nodes[nodes.length - 1]; + var blockNodes; + + for (var i = 1; node !== endNode && (node = node.nextSibling); i++) { + if (blockNodes || nodes[i] !== node) { + if (!blockNodes) { + blockNodes = jqLite(slice.call(nodes, 0, i)); + } + blockNodes.push(node); + } } - var element = startNode; - var elements = [element]; - - do { - element = element.nextSibling; - if (!element) break; - elements.push(element); - } while (element !== endNode); - - return jqLite(elements); + return blockNodes || nodes; } + /** - * @ngdoc interface + * Creates a new object without a prototype. This object is useful for lookup without having to + * guard against prototypically inherited properties via hasOwnProperty. + * + * Related micro-benchmarks: + * - http://jsperf.com/object-create2 + * - http://jsperf.com/proto-map-lookup/2 + * - http://jsperf.com/for-in-vs-object-keys2 + * + * @returns {Object} + */ + function createMap() { + return Object.create(null); + } + + function stringify(value) { + if (value == null) { // null || undefined + return ''; + } + switch (typeof value) { + case 'string': + break; + case 'number': + value = '' + value; + break; + default: + if (hasCustomToString(value) && !isArray(value) && !isDate(value)) { + value = value.toString(); + } else { + value = toJson(value); + } + } + + return value; + } + + var NODE_TYPE_ELEMENT = 1; + var NODE_TYPE_ATTRIBUTE = 2; + var NODE_TYPE_TEXT = 3; + var NODE_TYPE_COMMENT = 8; + var NODE_TYPE_DOCUMENT = 9; + var NODE_TYPE_DOCUMENT_FRAGMENT = 11; + + /** + * @ngdoc type * @name angular.Module + * @module ng * @description * * Interface for configuring angular {@link angular.module modules}. @@ -1465,6 +2230,7 @@ /** * @ngdoc function * @name angular.module + * @module ng * @description * * The `angular.module` is a global place for creating, registering and retrieving Angular @@ -1472,16 +2238,16 @@ * All modules (angular core or 3rd party) that should be available to an application must be * registered using this mechanism. * - * When passed two or more arguments, a new module is created. If passed only one argument, an - * existing module (the name passed as the first argument to `module`) is retrieved. + * Passing one argument retrieves an existing {@link angular.Module}, + * whereas passing more than one argument creates a new {@link angular.Module} * * * # Module * - * A module is a collection of services, directives, filters, and configuration information. - * `angular.module` is used to configure the {@link AUTO.$injector $injector}. + * A module is a collection of services, directives, controllers, filters, and configuration information. + * `angular.module` is used to configure the {@link auto.$injector $injector}. * - *
    +       * ```js
            * // Create a new module
            * var myModule = angular.module('myModule', []);
            *
    @@ -1489,30 +2255,33 @@
            * myModule.value('appName', 'MyCoolApp');
            *
            * // configure existing services inside initialization blocks.
    -       * myModule.config(function($locationProvider) {
    +       * myModule.config(['$locationProvider', function($locationProvider) {
          *   // Configure existing providers
          *   $locationProvider.hashPrefix('!');
    -     * });
    -       * 
    + * }]); + * ``` * * Then you can create an injector and load your modules like this: * - *
    -       * var injector = angular.injector(['ng', 'MyModule'])
    -       * 
    + * ```js + * var injector = angular.injector(['ng', 'myModule']) + * ``` * * However it's more likely that you'll just use * {@link ng.directive:ngApp ngApp} or * {@link angular.bootstrap} to simplify this process for you. * * @param {!string} name The name of the module to create or retrieve. - * @param {Array.=} requires If specified then new module is being created. If - * unspecified then the the module is being retrieved for further configuration. - * @param {Function} configFn Optional configuration function for the module. Same as - * {@link angular.Module#methods_config Module#config()}. - * @returns {module} new module with the {@link angular.Module} api. + * @param {!Array.=} requires If specified then new module is being created. If + * unspecified then the module is being retrieved for further configuration. + * @param {Function=} configFn Optional configuration function for the module. Same as + * {@link angular.Module#config Module#config()}. + * @returns {angular.Module} new module with the {@link angular.Module} api. */ return function module(name, requires, configFn) { + + var info = {}; + var assertNotHasOwnProperty = function(name, context) { if (name === 'hasOwnProperty') { throw ngMinErr('badname', 'hasOwnProperty is not a valid {0} name', context); @@ -1525,30 +2294,73 @@ } return ensure(modules, name, function() { if (!requires) { - throw $injectorMinErr('nomod', "Module '{0}' is not available! You either misspelled " + - "the module name or forgot to load it. If registering a module ensure that you " + - "specify the dependencies as the second argument.", name); + throw $injectorMinErr('nomod', 'Module \'{0}\' is not available! You either misspelled ' + + 'the module name or forgot to load it. If registering a module ensure that you ' + + 'specify the dependencies as the second argument.', name); } /** @type {!Array.>} */ var invokeQueue = []; + /** @type {!Array.} */ + var configBlocks = []; + /** @type {!Array.} */ var runBlocks = []; - var config = invokeLater('$injector', 'invoke'); + var config = invokeLater('$injector', 'invoke', 'push', configBlocks); /** @type {angular.Module} */ var moduleInstance = { // Private state _invokeQueue: invokeQueue, + _configBlocks: configBlocks, _runBlocks: runBlocks, + /** + * @ngdoc method + * @name angular.Module#info + * @module ng + * + * @param {Object=} info Information about the module + * @returns {Object|Module} The current info object for this module if called as a getter, + * or `this` if called as a setter. + * + * @description + * Read and write custom information about this module. + * For example you could put the version of the module in here. + * + * ```js + * angular.module('myModule', []).info({ version: '1.0.0' }); + * ``` + * + * The version could then be read back out by accessing the module elsewhere: + * + * ``` + * var version = angular.module('myModule').info().version; + * ``` + * + * You can also retrieve this information during runtime via the + * {@link $injector#modules `$injector.modules`} property: + * + * ```js + * var version = $injector.modules['myModule'].info().version; + * ``` + */ + info: function(value) { + if (isDefined(value)) { + if (!isObject(value)) throw ngMinErr('aobj', 'Argument \'{0}\' must be an object', 'value'); + info = value; + return this; + } + return info; + }, + /** * @ngdoc property * @name angular.Module#requires - * @propertyOf angular.Module - * @returns {Array.} List of module names which must be loaded before this module. + * @module ng + * * @description * Holds the list of modules which the injector will load before the current module is * loaded. @@ -1558,9 +2370,10 @@ /** * @ngdoc property * @name angular.Module#name - * @propertyOf angular.Module - * @returns {string} Name of the module. + * @module ng + * * @description + * Name of the module. */ name: name, @@ -1568,64 +2381,76 @@ /** * @ngdoc method * @name angular.Module#provider - * @methodOf angular.Module + * @module ng * @param {string} name service name * @param {Function} providerType Construction function for creating new instance of the * service. * @description - * See {@link AUTO.$provide#provider $provide.provider()}. + * See {@link auto.$provide#provider $provide.provider()}. */ - provider: invokeLater('$provide', 'provider'), + provider: invokeLaterAndSetModuleName('$provide', 'provider'), /** * @ngdoc method * @name angular.Module#factory - * @methodOf angular.Module + * @module ng * @param {string} name service name * @param {Function} providerFunction Function for creating new instance of the service. * @description - * See {@link AUTO.$provide#factory $provide.factory()}. + * See {@link auto.$provide#factory $provide.factory()}. */ - factory: invokeLater('$provide', 'factory'), + factory: invokeLaterAndSetModuleName('$provide', 'factory'), /** * @ngdoc method * @name angular.Module#service - * @methodOf angular.Module + * @module ng * @param {string} name service name * @param {Function} constructor A constructor function that will be instantiated. * @description - * See {@link AUTO.$provide#service $provide.service()}. + * See {@link auto.$provide#service $provide.service()}. */ - service: invokeLater('$provide', 'service'), + service: invokeLaterAndSetModuleName('$provide', 'service'), /** * @ngdoc method * @name angular.Module#value - * @methodOf angular.Module + * @module ng * @param {string} name service name * @param {*} object Service instance object. * @description - * See {@link AUTO.$provide#value $provide.value()}. + * See {@link auto.$provide#value $provide.value()}. */ value: invokeLater('$provide', 'value'), /** * @ngdoc method * @name angular.Module#constant - * @methodOf angular.Module + * @module ng * @param {string} name constant name * @param {*} object Constant value. * @description - * Because the constant are fixed, they get applied before other provide methods. - * See {@link AUTO.$provide#constant $provide.constant()}. + * Because the constants are fixed, they get applied before other provide methods. + * See {@link auto.$provide#constant $provide.constant()}. */ constant: invokeLater('$provide', 'constant', 'unshift'), + /** + * @ngdoc method + * @name angular.Module#decorator + * @module ng + * @param {string} name The name of the service to decorate. + * @param {Function} decorFn This function will be invoked when the service needs to be + * instantiated and should return the decorated service instance. + * @description + * See {@link auto.$provide#decorator $provide.decorator()}. + */ + decorator: invokeLaterAndSetModuleName('$provide', 'decorator', configBlocks), + /** * @ngdoc method * @name angular.Module#animation - * @methodOf angular.Module + * @module ng * @param {string} name animation name * @param {Function} animationFactory Factory function for creating new instance of an * animation. @@ -1635,9 +2460,9 @@ * * * Defines an animation hook that can be later used with - * {@link ngAnimate.$animate $animate} service and directives that use this service. + * {@link $animate $animate} service and directives that use this service. * - *
    +             * ```js
                  * module.animation('.animation-name', function($inject1, $inject2) {
                *   return {
                *     eventName : function(element, done) {
    @@ -1649,64 +2474,86 @@
                *     }
                *   }
                * })
    -             * 
    + * ``` * - * See {@link ngAnimate.$animateProvider#register $animateProvider.register()} and + * See {@link ng.$animateProvider#register $animateProvider.register()} and * {@link ngAnimate ngAnimate module} for more information. */ - animation: invokeLater('$animateProvider', 'register'), + animation: invokeLaterAndSetModuleName('$animateProvider', 'register'), /** * @ngdoc method * @name angular.Module#filter - * @methodOf angular.Module - * @param {string} name Filter name. + * @module ng + * @param {string} name Filter name - this must be a valid angular expression identifier * @param {Function} filterFactory Factory function for creating new instance of filter. * @description * See {@link ng.$filterProvider#register $filterProvider.register()}. + * + *
    + * **Note:** Filter names must be valid angular {@link expression} identifiers, such as `uppercase` or `orderBy`. + * Names with special characters, such as hyphens and dots, are not allowed. If you wish to namespace + * your filters, then you can use capitalization (`myappSubsectionFilterx`) or underscores + * (`myapp_subsection_filterx`). + *
    */ - filter: invokeLater('$filterProvider', 'register'), + filter: invokeLaterAndSetModuleName('$filterProvider', 'register'), /** * @ngdoc method * @name angular.Module#controller - * @methodOf angular.Module + * @module ng * @param {string|Object} name Controller name, or an object map of controllers where the * keys are the names and the values are the constructors. * @param {Function} constructor Controller constructor function. * @description * See {@link ng.$controllerProvider#register $controllerProvider.register()}. */ - controller: invokeLater('$controllerProvider', 'register'), + controller: invokeLaterAndSetModuleName('$controllerProvider', 'register'), /** * @ngdoc method * @name angular.Module#directive - * @methodOf angular.Module + * @module ng * @param {string|Object} name Directive name, or an object map of directives where the * keys are the names and the values are the factories. * @param {Function} directiveFactory Factory function for creating new instance of * directives. * @description - * See {@link ng.$compileProvider#methods_directive $compileProvider.directive()}. + * See {@link ng.$compileProvider#directive $compileProvider.directive()}. */ - directive: invokeLater('$compileProvider', 'directive'), + directive: invokeLaterAndSetModuleName('$compileProvider', 'directive'), + + /** + * @ngdoc method + * @name angular.Module#component + * @module ng + * @param {string} name Name of the component in camel-case (i.e. myComp which will match as my-comp) + * @param {Object} options Component definition object (a simplified + * {@link ng.$compile#directive-definition-object directive definition object}) + * + * @description + * See {@link ng.$compileProvider#component $compileProvider.component()}. + */ + component: invokeLaterAndSetModuleName('$compileProvider', 'component'), /** * @ngdoc method * @name angular.Module#config - * @methodOf angular.Module + * @module ng * @param {Function} configFn Execute this function on module load. Useful for service * configuration. * @description * Use this method to register work which needs to be performed on module loading. + * For more about how to configure services, see + * {@link providers#provider-recipe Provider Recipe}. */ config: config, /** * @ngdoc method * @name angular.Module#run - * @methodOf angular.Module + * @module ng * @param {Function} initializationFn Execute this function after injector creation. * Useful for application initialization. * @description @@ -1723,7 +2570,7 @@ config(configFn); } - return moduleInstance; + return moduleInstance; /** * @param {string} provider @@ -1731,9 +2578,24 @@ * @param {String=} insertMethod * @returns {angular.Module} */ - function invokeLater(provider, method, insertMethod) { + function invokeLater(provider, method, insertMethod, queue) { + if (!queue) queue = invokeQueue; return function() { - invokeQueue[insertMethod || 'push']([provider, method, arguments]); + queue[insertMethod || 'push']([provider, method, arguments]); + return moduleInstance; + }; + } + + /** + * @param {string} provider + * @param {string} method + * @returns {angular.Module} + */ + function invokeLaterAndSetModuleName(provider, method, queue) { + if (!queue) queue = invokeQueue; + return function(recipeName, factoryFunction) { + if (factoryFunction && isFunction(factoryFunction)) factoryFunction.$$moduleName = name; + queue.push([provider, method, arguments]); return moduleInstance; }; } @@ -1743,88 +2605,175 @@ } - /* global - angularModule: true, - version: true, + /* global shallowCopy: true */ - $LocaleProvider, - $CompileProvider, - - htmlAnchorDirective, - inputDirective, - inputDirective, - formDirective, - scriptDirective, - selectDirective, - styleDirective, - optionDirective, - ngBindDirective, - ngBindHtmlDirective, - ngBindTemplateDirective, - ngClassDirective, - ngClassEvenDirective, - ngClassOddDirective, - ngCspDirective, - ngCloakDirective, - ngControllerDirective, - ngFormDirective, - ngHideDirective, - ngIfDirective, - ngIncludeDirective, - ngIncludeFillContentDirective, - ngInitDirective, - ngNonBindableDirective, - ngPluralizeDirective, - ngRepeatDirective, - ngShowDirective, - ngStyleDirective, - ngSwitchDirective, - ngSwitchWhenDirective, - ngSwitchDefaultDirective, - ngOptionsDirective, - ngTranscludeDirective, - ngModelDirective, - ngListDirective, - ngChangeDirective, - requiredDirective, - requiredDirective, - ngValueDirective, - ngAttributeAliasDirectives, - ngEventDirectives, - - $AnchorScrollProvider, - $AnimateProvider, - $BrowserProvider, - $CacheFactoryProvider, - $ControllerProvider, - $DocumentProvider, - $ExceptionHandlerProvider, - $FilterProvider, - $InterpolateProvider, - $IntervalProvider, - $HttpProvider, - $HttpBackendProvider, - $LocationProvider, - $LogProvider, - $ParseProvider, - $RootScopeProvider, - $QProvider, - $$SanitizeUriProvider, - $SceProvider, - $SceDelegateProvider, - $SnifferProvider, - $TemplateCacheProvider, - $TimeoutProvider, - $WindowProvider + /** + * Creates a shallow copy of an object, an array or a primitive. + * + * Assumes that there are no proto properties for objects. */ + function shallowCopy(src, dst) { + if (isArray(src)) { + dst = dst || []; + + for (var i = 0, ii = src.length; i < ii; i++) { + dst[i] = src[i]; + } + } else if (isObject(src)) { + dst = dst || {}; + + for (var key in src) { + if (!(key.charAt(0) === '$' && key.charAt(1) === '$')) { + dst[key] = src[key]; + } + } + } + + return dst || src; + } + + /* exported toDebugString */ + + function serializeObject(obj, maxDepth) { + var seen = []; + + // There is no direct way to stringify object until reaching a specific depth + // and a very deep object can cause a performance issue, so we copy the object + // based on this specific depth and then stringify it. + if (isValidObjectMaxDepth(maxDepth)) { + // This file is also included in `angular-loader`, so `copy()` might not always be available in + // the closure. Therefore, it is lazily retrieved as `angular.copy()` when needed. + obj = angular.copy(obj, null, maxDepth); + } + return JSON.stringify(obj, function(key, val) { + val = toJsonReplacer(key, val); + if (isObject(val)) { + + if (seen.indexOf(val) >= 0) return '...'; + + seen.push(val); + } + return val; + }); + } + + function toDebugString(obj, maxDepth) { + if (typeof obj === 'function') { + return obj.toString().replace(/ \{[\s\S]*$/, ''); + } else if (isUndefined(obj)) { + return 'undefined'; + } else if (typeof obj !== 'string') { + return serializeObject(obj, maxDepth); + } + return obj; + } + + /* global angularModule: true, + version: true, + + $CompileProvider, + + htmlAnchorDirective, + inputDirective, + inputDirective, + formDirective, + scriptDirective, + selectDirective, + optionDirective, + ngBindDirective, + ngBindHtmlDirective, + ngBindTemplateDirective, + ngClassDirective, + ngClassEvenDirective, + ngClassOddDirective, + ngCloakDirective, + ngControllerDirective, + ngFormDirective, + ngHideDirective, + ngIfDirective, + ngIncludeDirective, + ngIncludeFillContentDirective, + ngInitDirective, + ngNonBindableDirective, + ngPluralizeDirective, + ngRepeatDirective, + ngShowDirective, + ngStyleDirective, + ngSwitchDirective, + ngSwitchWhenDirective, + ngSwitchDefaultDirective, + ngOptionsDirective, + ngTranscludeDirective, + ngModelDirective, + ngListDirective, + ngChangeDirective, + patternDirective, + patternDirective, + requiredDirective, + requiredDirective, + minlengthDirective, + minlengthDirective, + maxlengthDirective, + maxlengthDirective, + ngValueDirective, + ngModelOptionsDirective, + ngAttributeAliasDirectives, + ngEventDirectives, + + $AnchorScrollProvider, + $AnimateProvider, + $CoreAnimateCssProvider, + $$CoreAnimateJsProvider, + $$CoreAnimateQueueProvider, + $$AnimateRunnerFactoryProvider, + $$AnimateAsyncRunFactoryProvider, + $BrowserProvider, + $CacheFactoryProvider, + $ControllerProvider, + $DateProvider, + $DocumentProvider, + $$IsDocumentHiddenProvider, + $ExceptionHandlerProvider, + $FilterProvider, + $$ForceReflowProvider, + $InterpolateProvider, + $IntervalProvider, + $HttpProvider, + $HttpParamSerializerProvider, + $HttpParamSerializerJQLikeProvider, + $HttpBackendProvider, + $xhrFactoryProvider, + $jsonpCallbacksProvider, + $LocationProvider, + $LogProvider, + $$MapProvider, + $ParseProvider, + $RootScopeProvider, + $QProvider, + $$QProvider, + $$SanitizeUriProvider, + $SceProvider, + $SceDelegateProvider, + $SnifferProvider, + $TemplateCacheProvider, + $TemplateRequestProvider, + $$TestabilityProvider, + $TimeoutProvider, + $$RAFProvider, + $WindowProvider, + $$jqLiteProvider, + $$CookieReaderProvider +*/ /** - * @ngdoc property + * @ngdoc object * @name angular.version + * @module ng * @description - * An object that contains information about the current AngularJS version. This object has the - * following properties: + * An object that contains information about the current AngularJS version. + * + * This object has the following properties: * * - `full` – `{string}` – Full version string, such as "0.9.18". * - `major` – `{number}` – Major version number, such as "0". @@ -1833,28 +2782,32 @@ * - `codeName` – `{string}` – Code name of the release, such as "jiggling-armfat". */ var version = { - full: '1.2.8', // all of these placeholder strings will be replaced by grunt's - major: 1, // package task - minor: 2, - dot: 8, - codeName: 'interdimensional-cartography' + // These placeholder strings will be replaced by grunt's `build` task. + // They need to be double- or single-quoted. + full: '1.6.6', + major: 1, + minor: 6, + dot: 6, + codeName: 'interdimensional-cable' }; - function publishExternalAPI(angular){ + function publishExternalAPI(angular) { extend(angular, { + 'errorHandlingConfig': errorHandlingConfig, 'bootstrap': bootstrap, 'copy': copy, 'extend': extend, + 'merge': merge, 'equals': equals, 'element': jqLite, 'forEach': forEach, 'injector': createInjector, - 'noop':noop, - 'bind':bind, + 'noop': noop, + 'bind': bind, 'toJson': toJson, 'fromJson': fromJson, - 'identity':identity, + 'identity': identity, 'isUndefined': isUndefined, 'isDefined': isDefined, 'isString': isString, @@ -1867,17 +2820,17 @@ 'isDate': isDate, 'lowercase': lowercase, 'uppercase': uppercase, - 'callbacks': {counter: 0}, + 'callbacks': {$$counter: 0}, + 'getTestability': getTestability, + 'reloadWithDebugInfo': reloadWithDebugInfo, '$$minErr': minErr, - '$$csp': csp + '$$csp': csp, + '$$encodeUriSegment': encodeUriSegment, + '$$encodeUriQuery': encodeUriQuery, + '$$stringify': stringify }); angularModule = setupModuleLoader(window); - try { - angularModule('ngLocale'); - } catch (e) { - angularModule('ngLocale', []).provider('$locale', $LocaleProvider); - } angularModule('ng', ['ngLocale'], ['$provide', function ngModule($provide) { @@ -1886,86 +2839,120 @@ $$sanitizeUri: $$SanitizeUriProvider }); $provide.provider('$compile', $CompileProvider). - directive({ - a: htmlAnchorDirective, - input: inputDirective, - textarea: inputDirective, - form: formDirective, - script: scriptDirective, - select: selectDirective, - style: styleDirective, - option: optionDirective, - ngBind: ngBindDirective, - ngBindHtml: ngBindHtmlDirective, - ngBindTemplate: ngBindTemplateDirective, - ngClass: ngClassDirective, - ngClassEven: ngClassEvenDirective, - ngClassOdd: ngClassOddDirective, - ngCloak: ngCloakDirective, - ngController: ngControllerDirective, - ngForm: ngFormDirective, - ngHide: ngHideDirective, - ngIf: ngIfDirective, - ngInclude: ngIncludeDirective, - ngInit: ngInitDirective, - ngNonBindable: ngNonBindableDirective, - ngPluralize: ngPluralizeDirective, - ngRepeat: ngRepeatDirective, - ngShow: ngShowDirective, - ngStyle: ngStyleDirective, - ngSwitch: ngSwitchDirective, - ngSwitchWhen: ngSwitchWhenDirective, - ngSwitchDefault: ngSwitchDefaultDirective, - ngOptions: ngOptionsDirective, - ngTransclude: ngTranscludeDirective, - ngModel: ngModelDirective, - ngList: ngListDirective, - ngChange: ngChangeDirective, - required: requiredDirective, - ngRequired: requiredDirective, - ngValue: ngValueDirective - }). - directive({ - ngInclude: ngIncludeFillContentDirective - }). - directive(ngAttributeAliasDirectives). - directive(ngEventDirectives); + directive({ + a: htmlAnchorDirective, + input: inputDirective, + textarea: inputDirective, + form: formDirective, + script: scriptDirective, + select: selectDirective, + option: optionDirective, + ngBind: ngBindDirective, + ngBindHtml: ngBindHtmlDirective, + ngBindTemplate: ngBindTemplateDirective, + ngClass: ngClassDirective, + ngClassEven: ngClassEvenDirective, + ngClassOdd: ngClassOddDirective, + ngCloak: ngCloakDirective, + ngController: ngControllerDirective, + ngForm: ngFormDirective, + ngHide: ngHideDirective, + ngIf: ngIfDirective, + ngInclude: ngIncludeDirective, + ngInit: ngInitDirective, + ngNonBindable: ngNonBindableDirective, + ngPluralize: ngPluralizeDirective, + ngRepeat: ngRepeatDirective, + ngShow: ngShowDirective, + ngStyle: ngStyleDirective, + ngSwitch: ngSwitchDirective, + ngSwitchWhen: ngSwitchWhenDirective, + ngSwitchDefault: ngSwitchDefaultDirective, + ngOptions: ngOptionsDirective, + ngTransclude: ngTranscludeDirective, + ngModel: ngModelDirective, + ngList: ngListDirective, + ngChange: ngChangeDirective, + pattern: patternDirective, + ngPattern: patternDirective, + required: requiredDirective, + ngRequired: requiredDirective, + minlength: minlengthDirective, + ngMinlength: minlengthDirective, + maxlength: maxlengthDirective, + ngMaxlength: maxlengthDirective, + ngValue: ngValueDirective, + ngModelOptions: ngModelOptionsDirective + }). + directive({ + ngInclude: ngIncludeFillContentDirective + }). + directive(ngAttributeAliasDirectives). + directive(ngEventDirectives); $provide.provider({ $anchorScroll: $AnchorScrollProvider, $animate: $AnimateProvider, + $animateCss: $CoreAnimateCssProvider, + $$animateJs: $$CoreAnimateJsProvider, + $$animateQueue: $$CoreAnimateQueueProvider, + $$AnimateRunner: $$AnimateRunnerFactoryProvider, + $$animateAsyncRun: $$AnimateAsyncRunFactoryProvider, $browser: $BrowserProvider, $cacheFactory: $CacheFactoryProvider, $controller: $ControllerProvider, $document: $DocumentProvider, + $$isDocumentHidden: $$IsDocumentHiddenProvider, $exceptionHandler: $ExceptionHandlerProvider, $filter: $FilterProvider, + $$forceReflow: $$ForceReflowProvider, $interpolate: $InterpolateProvider, $interval: $IntervalProvider, $http: $HttpProvider, + $httpParamSerializer: $HttpParamSerializerProvider, + $httpParamSerializerJQLike: $HttpParamSerializerJQLikeProvider, $httpBackend: $HttpBackendProvider, + $xhrFactory: $xhrFactoryProvider, + $jsonpCallbacks: $jsonpCallbacksProvider, $location: $LocationProvider, $log: $LogProvider, $parse: $ParseProvider, $rootScope: $RootScopeProvider, $q: $QProvider, + $$q: $$QProvider, $sce: $SceProvider, $sceDelegate: $SceDelegateProvider, $sniffer: $SnifferProvider, $templateCache: $TemplateCacheProvider, + $templateRequest: $TemplateRequestProvider, + $$testability: $$TestabilityProvider, $timeout: $TimeoutProvider, - $window: $WindowProvider + $window: $WindowProvider, + $$rAF: $$RAFProvider, + $$jqLite: $$jqLiteProvider, + $$Map: $$MapProvider, + $$cookieReader: $$CookieReaderProvider }); } - ]); + ]) + .info({ angularVersion: '1.6.6' }); } - /* global + /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * Any commits to this file should be reviewed with security in mind. * + * Changes to this file can potentially create security vulnerabilities. * + * An approval from 2 Core members with history of modifying * + * this file is required. * + * * + * Does the change somehow allow for arbitrary javascript to be executed? * + * Or allows for someone to change the prototype of built-in objects? * + * Or gives undesired access to variables likes document or window? * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - -JQLitePrototype, - -addEventListenerFn, - -removeEventListenerFn, - -BOOLEAN_ATTR - */ + /* global + JQLitePrototype: true, + BOOLEAN_ATTR: true, + ALIASED_ATTR: true +*/ ////////////////////////////////// //JQLite @@ -1974,37 +2961,46 @@ /** * @ngdoc function * @name angular.element - * @function + * @module ng + * @kind function * * @description * Wraps a raw DOM element or HTML string as a [jQuery](http://jquery.com) element. * * If jQuery is available, `angular.element` is an alias for the * [jQuery](http://api.jquery.com/jQuery/) function. If jQuery is not available, `angular.element` - * delegates to Angular's built-in subset of jQuery, called "jQuery lite" or "jqLite." + * delegates to Angular's built-in subset of jQuery, called "jQuery lite" or **jqLite**. * - *
    jqLite is a tiny, API-compatible subset of jQuery that allows - * Angular to manipulate the DOM in a cross-browser compatible way. **jqLite** implements only the most - * commonly needed functionality with the goal of having a very small footprint.
    + * jqLite is a tiny, API-compatible subset of jQuery that allows + * Angular to manipulate the DOM in a cross-browser compatible way. jqLite implements only the most + * commonly needed functionality with the goal of having a very small footprint. * - * To use jQuery, simply load it before `DOMContentLoaded` event fired. + * To use `jQuery`, simply ensure it is loaded before the `angular.js` file. You can also use the + * {@link ngJq `ngJq`} directive to specify that jqlite should be used over jQuery, or to use a + * specific version of jQuery if multiple versions exist on the page. * - *
    **Note:** all element references in Angular are always wrapped with jQuery or - * jqLite; they are never raw DOM references.
    + *
    **Note:** All element references in Angular are always wrapped with jQuery or + * jqLite (such as the element argument in a directive's compile / link function). They are never raw DOM references.
    + * + *
    **Note:** Keep in mind that this function will not find elements + * by tag name / CSS selector. For lookups by tag name, try instead `angular.element(document).find(...)` + * or `$document.find()`, or use the standard DOM APIs, e.g. `document.querySelectorAll()`.
    * * ## Angular's jqLite * jqLite provides only the following jQuery methods: * - * - [`addClass()`](http://api.jquery.com/addClass/) + * - [`addClass()`](http://api.jquery.com/addClass/) - Does not support a function as first argument * - [`after()`](http://api.jquery.com/after/) * - [`append()`](http://api.jquery.com/append/) - * - [`attr()`](http://api.jquery.com/attr/) - * - [`bind()`](http://api.jquery.com/on/) - Does not support namespaces, selectors or eventData + * - [`attr()`](http://api.jquery.com/attr/) - Does not support functions as parameters + * - [`bind()`](http://api.jquery.com/bind/) (_deprecated_, use [`on()`](http://api.jquery.com/on/)) - Does not support namespaces, selectors or eventData * - [`children()`](http://api.jquery.com/children/) - Does not support selectors * - [`clone()`](http://api.jquery.com/clone/) * - [`contents()`](http://api.jquery.com/contents/) - * - [`css()`](http://api.jquery.com/css/) + * - [`css()`](http://api.jquery.com/css/) - Only retrieves inline-styles, does not call `getComputedStyle()`. + * As a setter, does not convert numbers to strings or append 'px', and also does not have automatic property prefixing. * - [`data()`](http://api.jquery.com/data/) + * - [`detach()`](http://api.jquery.com/detach/) * - [`empty()`](http://api.jquery.com/empty/) * - [`eq()`](http://api.jquery.com/eq/) * - [`find()`](http://api.jquery.com/find/) - Limited to lookups by tag name @@ -2012,21 +3008,21 @@ * - [`html()`](http://api.jquery.com/html/) * - [`next()`](http://api.jquery.com/next/) - Does not support selectors * - [`on()`](http://api.jquery.com/on/) - Does not support namespaces, selectors or eventData - * - [`off()`](http://api.jquery.com/off/) - Does not support namespaces or selectors + * - [`off()`](http://api.jquery.com/off/) - Does not support namespaces, selectors or event object as parameter * - [`one()`](http://api.jquery.com/one/) - Does not support namespaces or selectors * - [`parent()`](http://api.jquery.com/parent/) - Does not support selectors * - [`prepend()`](http://api.jquery.com/prepend/) * - [`prop()`](http://api.jquery.com/prop/) - * - [`ready()`](http://api.jquery.com/ready/) + * - [`ready()`](http://api.jquery.com/ready/) (_deprecated_, use `angular.element(callback)` instead of `angular.element(document).ready(callback)`) * - [`remove()`](http://api.jquery.com/remove/) - * - [`removeAttr()`](http://api.jquery.com/removeAttr/) - * - [`removeClass()`](http://api.jquery.com/removeClass/) + * - [`removeAttr()`](http://api.jquery.com/removeAttr/) - Does not support multiple attributes + * - [`removeClass()`](http://api.jquery.com/removeClass/) - Does not support a function as first argument * - [`removeData()`](http://api.jquery.com/removeData/) * - [`replaceWith()`](http://api.jquery.com/replaceWith/) * - [`text()`](http://api.jquery.com/text/) - * - [`toggleClass()`](http://api.jquery.com/toggleClass/) - * - [`triggerHandler()`](http://api.jquery.com/triggerHandler/) - Passes a dummy event object to handlers. - * - [`unbind()`](http://api.jquery.com/off/) - Does not support namespaces + * - [`toggleClass()`](http://api.jquery.com/toggleClass/) - Does not support a function as first argument + * - [`triggerHandler()`](http://api.jquery.com/triggerHandler/) - Passes a dummy event object to handlers + * - [`unbind()`](http://api.jquery.com/unbind/) (_deprecated_, use [`off()`](http://api.jquery.com/off/)) - Does not support namespaces or event object as parameter * - [`val()`](http://api.jquery.com/val/) * - [`wrap()`](http://api.jquery.com/wrap/) * @@ -2044,112 +3040,196 @@ * camelCase directive name, then the controller for this directive will be retrieved (e.g. * `'ngModel'`). * - `injector()` - retrieves the injector of the current element or its parent. - * - `scope()` - retrieves the {@link api/ng.$rootScope.Scope scope} of the current - * element or its parent. - * - `isolateScope()` - retrieves an isolate {@link ../api/ng.$rootScope.Scope scope} if one is attached directly to the + * - `scope()` - retrieves the {@link ng.$rootScope.Scope scope} of the current + * element or its parent. Requires {@link guide/production#disabling-debug-data Debug Data} to + * be enabled. + * - `isolateScope()` - retrieves an isolate {@link ng.$rootScope.Scope scope} if one is attached directly to the * current element. This getter should be used only on elements that contain a directive which starts a new isolate * scope. Calling `scope()` on this element always returns the original non-isolate scope. + * Requires {@link guide/production#disabling-debug-data Debug Data} to be enabled. * - `inheritedData()` - same as `data()`, but walks up the DOM until a value is found or the top * parent element is reached. * + * @knownIssue You cannot spy on `angular.element` if you are using Jasmine version 1.x. See + * https://github.com/angular/angular.js/issues/14251 for more information. + * * @param {string|DOMElement} element HTML string or DOMElement to be wrapped into jQuery. * @returns {Object} jQuery object. */ + JQLite.expando = 'ng339'; + var jqCache = JQLite.cache = {}, - jqName = JQLite.expando = 'ng-' + new Date().getTime(), - jqId = 1, - addEventListenerFn = (window.document.addEventListener - ? function(element, type, fn) {element.addEventListener(type, fn, false);} - : function(element, type, fn) {element.attachEvent('on' + type, fn);}), - removeEventListenerFn = (window.document.removeEventListener - ? function(element, type, fn) {element.removeEventListener(type, fn, false); } - : function(element, type, fn) {element.detachEvent('on' + type, fn); }); + jqId = 1; + + /* + * !!! This is an undocumented "private" function !!! + */ + JQLite._data = function(node) { + //jQuery always returns an object on cache miss + return this.cache[node[this.expando]] || {}; + }; function jqNextId() { return ++jqId; } - var SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g; - var MOZ_HACK_REGEXP = /^moz([A-Z])/; + var DASH_LOWERCASE_REGEXP = /-([a-z])/g; + var MS_HACK_REGEXP = /^-ms-/; + var MOUSE_EVENT_MAP = { mouseleave: 'mouseout', mouseenter: 'mouseover' }; var jqLiteMinErr = minErr('jqLite'); /** - * Converts snake_case to camelCase. - * Also there is special case for Moz prefix starting with upper case letter. + * Converts kebab-case to camelCase. + * There is also a special case for the ms prefix starting with a lowercase letter. * @param name Name to normalize */ - function camelCase(name) { - return name. - replace(SPECIAL_CHARS_REGEXP, function(_, separator, letter, offset) { - return offset ? letter.toUpperCase() : letter; - }). - replace(MOZ_HACK_REGEXP, 'Moz$1'); + function cssKebabToCamel(name) { + return kebabToCamel(name.replace(MS_HACK_REGEXP, 'ms-')); } -///////////////////////////////////////////// -// jQuery mutation patch -// -// In conjunction with bindJQuery intercepts all jQuery's DOM destruction apis and fires a -// $destroy event on all DOM nodes being removed. -// -///////////////////////////////////////////// + function fnCamelCaseReplace(all, letter) { + return letter.toUpperCase(); + } - function jqLitePatchJQueryRemove(name, dispatchThis, filterElems, getterIfNoArguments) { - var originalJqFn = jQuery.fn[name]; - originalJqFn = originalJqFn.$original || originalJqFn; - removePatch.$original = originalJqFn; - jQuery.fn[name] = removePatch; + /** + * Converts kebab-case to camelCase. + * @param name Name to normalize + */ + function kebabToCamel(name) { + return name + .replace(DASH_LOWERCASE_REGEXP, fnCamelCaseReplace); + } - function removePatch(param) { - // jshint -W040 - var list = filterElems && param ? [this.filter(param)] : [this], - fireEvent = dispatchThis, - set, setIndex, setLength, - element, childIndex, childLength, children; + var SINGLE_TAG_REGEXP = /^<([\w-]+)\s*\/?>(?:<\/\1>|)$/; + var HTML_REGEXP = /<|&#?\w+;/; + var TAG_NAME_REGEXP = /<([\w:-]+)/; + var XHTML_TAG_REGEXP = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi; - if (!getterIfNoArguments || param != null) { - while(list.length) { - set = list.shift(); - for(setIndex = 0, setLength = set.length; setIndex < setLength; setIndex++) { - element = jqLite(set[setIndex]); - if (fireEvent) { - element.triggerHandler('$destroy'); - } else { - fireEvent = !fireEvent; - } - for(childIndex = 0, childLength = (children = element.children()).length; - childIndex < childLength; - childIndex++) { - list.push(jQuery(children[childIndex])); - } - } - } - } - return originalJqFn.apply(this, arguments); + var wrapMap = { + 'option': [1, ''], + + 'thead': [1, '
    ', '
    '], + 'col': [2, '', '
    '], + 'tr': [2, '', '
    '], + 'td': [3, '', '
    '], + '_default': [0, '', ''] + }; + + wrapMap.optgroup = wrapMap.option; + wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; + wrapMap.th = wrapMap.td; + + + function jqLiteIsTextNode(html) { + return !HTML_REGEXP.test(html); + } + + function jqLiteAcceptsData(node) { + // The window object can accept data but has no nodeType + // Otherwise we are only interested in elements (1) and documents (9) + var nodeType = node.nodeType; + return nodeType === NODE_TYPE_ELEMENT || !nodeType || nodeType === NODE_TYPE_DOCUMENT; + } + + function jqLiteHasData(node) { + for (var key in jqCache[node.ng339]) { + return true; } + return false; } + function jqLiteBuildFragment(html, context) { + var tmp, tag, wrap, + fragment = context.createDocumentFragment(), + nodes = [], i; + + if (jqLiteIsTextNode(html)) { + // Convert non-html into a text node + nodes.push(context.createTextNode(html)); + } else { + // Convert html into DOM nodes + tmp = fragment.appendChild(context.createElement('div')); + tag = (TAG_NAME_REGEXP.exec(html) || ['', ''])[1].toLowerCase(); + wrap = wrapMap[tag] || wrapMap._default; + tmp.innerHTML = wrap[1] + html.replace(XHTML_TAG_REGEXP, '<$1>') + wrap[2]; + + // Descend through wrappers to the right content + i = wrap[0]; + while (i--) { + tmp = tmp.lastChild; + } + + nodes = concat(nodes, tmp.childNodes); + + tmp = fragment.firstChild; + tmp.textContent = ''; + } + + // Remove wrapper from fragment + fragment.textContent = ''; + fragment.innerHTML = ''; // Clear inner HTML + forEach(nodes, function(node) { + fragment.appendChild(node); + }); + + return fragment; + } + + function jqLiteParseHTML(html, context) { + context = context || window.document; + var parsed; + + if ((parsed = SINGLE_TAG_REGEXP.exec(html))) { + return [context.createElement(parsed[1])]; + } + + if ((parsed = jqLiteBuildFragment(html, context))) { + return parsed.childNodes; + } + + return []; + } + + function jqLiteWrapNode(node, wrapper) { + var parent = node.parentNode; + + if (parent) { + parent.replaceChild(wrapper, node); + } + + wrapper.appendChild(node); + } + + +// IE9-11 has no method "contains" in SVG element and in Node.prototype. Bug #10259. + var jqLiteContains = window.Node.prototype.contains || /** @this */ function(arg) { + // eslint-disable-next-line no-bitwise + return !!(this.compareDocumentPosition(arg) & 16); + }; + ///////////////////////////////////////////// function JQLite(element) { if (element instanceof JQLite) { return element; } + + var argIsString; + + if (isString(element)) { + element = trim(element); + argIsString = true; + } if (!(this instanceof JQLite)) { - if (isString(element) && element.charAt(0) != '<') { - throw jqLiteMinErr('nosel', 'Looking up elements via selectors is not supported by jqLite! See: http://docs.angularjs.org/../api/angular.element'); + if (argIsString && element.charAt(0) !== '<') { + throw jqLiteMinErr('nosel', 'Looking up elements via selectors is not supported by jqLite! See: http://docs.angularjs.org/api/angular.element'); } return new JQLite(element); } - if (isString(element)) { - var div = document.createElement('div'); - // Read about the NoScope elements here: - // http://msdn.microsoft.com/en-us/library/ms533897(VS.85).aspx - div.innerHTML = '
     
    ' + element; // IE insanity to make NoScope elements work! - div.removeChild(div.firstChild); // remove the superfluous div - jqLiteAddNodes(this, div.childNodes); - var fragment = jqLite(document.createDocumentFragment()); - fragment.append(this); // detach the elements from the temporary DOM div. + if (argIsString) { + jqLiteAddNodes(this, jqLiteParseHTML(element)); + } else if (isFunction(element)) { + jqLiteReady(element); } else { jqLiteAddNodes(this, element); } @@ -2159,111 +3239,129 @@ return element.cloneNode(true); } - function jqLiteDealoc(element){ - jqLiteRemoveData(element); - for ( var i = 0, children = element.childNodes || []; i < children.length; i++) { - jqLiteDealoc(children[i]); + function jqLiteDealoc(element, onlyDescendants) { + if (!onlyDescendants && jqLiteAcceptsData(element)) jqLite.cleanData([element]); + + if (element.querySelectorAll) { + jqLite.cleanData(element.querySelectorAll('*')); } } function jqLiteOff(element, type, fn, unsupported) { if (isDefined(unsupported)) throw jqLiteMinErr('offargs', 'jqLite#off() does not support the `selector` argument'); - var events = jqLiteExpandoStore(element, 'events'), - handle = jqLiteExpandoStore(element, 'handle'); + var expandoStore = jqLiteExpandoStore(element); + var events = expandoStore && expandoStore.events; + var handle = expandoStore && expandoStore.handle; if (!handle) return; //no listeners registered - if (isUndefined(type)) { - forEach(events, function(eventHandler, type) { - removeEventListenerFn(element, type, eventHandler); + if (!type) { + for (type in events) { + if (type !== '$destroy') { + element.removeEventListener(type, handle); + } delete events[type]; - }); + } } else { - forEach(type.split(' '), function(type) { - if (isUndefined(fn)) { - removeEventListenerFn(element, type, events[type]); + + var removeHandler = function(type) { + var listenerFns = events[type]; + if (isDefined(fn)) { + arrayRemove(listenerFns || [], fn); + } + if (!(isDefined(fn) && listenerFns && listenerFns.length > 0)) { + element.removeEventListener(type, handle); delete events[type]; - } else { - arrayRemove(events[type] || [], fn); + } + }; + + forEach(type.split(' '), function(type) { + removeHandler(type); + if (MOUSE_EVENT_MAP[type]) { + removeHandler(MOUSE_EVENT_MAP[type]); } }); } } function jqLiteRemoveData(element, name) { - var expandoId = element[jqName], - expandoStore = jqCache[expandoId]; + var expandoId = element.ng339; + var expandoStore = expandoId && jqCache[expandoId]; if (expandoStore) { if (name) { - delete jqCache[expandoId].data[name]; + delete expandoStore.data[name]; return; } if (expandoStore.handle) { - expandoStore.events.$destroy && expandoStore.handle({}, '$destroy'); + if (expandoStore.events.$destroy) { + expandoStore.handle({}, '$destroy'); + } jqLiteOff(element); } delete jqCache[expandoId]; - element[jqName] = undefined; // ie does not allow deletion of attributes on elements. + element.ng339 = undefined; // don't delete DOM expandos. IE and Chrome don't like it } } - function jqLiteExpandoStore(element, key, value) { - var expandoId = element[jqName], - expandoStore = jqCache[expandoId || -1]; - if (isDefined(value)) { - if (!expandoStore) { - element[jqName] = expandoId = jqNextId(); - expandoStore = jqCache[expandoId] = {}; - } - expandoStore[key] = value; - } else { - return expandoStore && expandoStore[key]; + function jqLiteExpandoStore(element, createIfNecessary) { + var expandoId = element.ng339, + expandoStore = expandoId && jqCache[expandoId]; + + if (createIfNecessary && !expandoStore) { + element.ng339 = expandoId = jqNextId(); + expandoStore = jqCache[expandoId] = {events: {}, data: {}, handle: undefined}; } + + return expandoStore; } + function jqLiteData(element, key, value) { - var data = jqLiteExpandoStore(element, 'data'), - isSetter = isDefined(value), - keyDefined = !isSetter && isDefined(key), - isSimpleGetter = keyDefined && !isObject(key); + if (jqLiteAcceptsData(element)) { + var prop; - if (!data && !isSimpleGetter) { - jqLiteExpandoStore(element, 'data', data = {}); - } + var isSimpleSetter = isDefined(value); + var isSimpleGetter = !isSimpleSetter && key && !isObject(key); + var massGetter = !key; + var expandoStore = jqLiteExpandoStore(element, !isSimpleGetter); + var data = expandoStore && expandoStore.data; - if (isSetter) { - data[key] = value; - } else { - if (keyDefined) { - if (isSimpleGetter) { - // don't create data in this case. - return data && data[key]; - } else { - extend(data, key); - } + if (isSimpleSetter) { // data('key', value) + data[kebabToCamel(key)] = value; } else { - return data; + if (massGetter) { // data() + return data; + } else { + if (isSimpleGetter) { // data('key') + // don't force creation of expandoStore if it doesn't exist yet + return data && data[kebabToCamel(key)]; + } else { // mass-setter: data({key1: val1, key2: val2}) + for (prop in key) { + data[kebabToCamel(prop)] = key[prop]; + } + } + } } } } function jqLiteHasClass(element, selector) { if (!element.getAttribute) return false; - return ((" " + (element.getAttribute('class') || '') + " ").replace(/[\n\t]/g, " "). - indexOf( " " + selector + " " ) > -1); + return ((' ' + (element.getAttribute('class') || '') + ' ').replace(/[\n\t]/g, ' '). + indexOf(' ' + selector + ' ') > -1); } function jqLiteRemoveClass(element, cssClasses) { if (cssClasses && element.setAttribute) { forEach(cssClasses.split(' '), function(cssClass) { element.setAttribute('class', trim( - (" " + (element.getAttribute('class') || '') + " ") - .replace(/[\n\t]/g, " ") - .replace(" " + trim(cssClass) + " ", " ")) + (' ' + (element.getAttribute('class') || '') + ' ') + .replace(/[\n\t]/g, ' ') + .replace(' ' + trim(cssClass) + ' ', ' ')) ); }); } @@ -2272,7 +3370,7 @@ function jqLiteAddClass(element, cssClasses) { if (cssClasses && element.setAttribute) { var existingClasses = (' ' + (element.getAttribute('class') || '') + ' ') - .replace(/[\n\t]/g, " "); + .replace(/[\n\t]/g, ' '); forEach(cssClasses.split(' '), function(cssClass) { cssClass = trim(cssClass); @@ -2285,76 +3383,113 @@ } } + function jqLiteAddNodes(root, elements) { + // THIS CODE IS VERY HOT. Don't make changes without benchmarking. + if (elements) { - elements = (!elements.nodeName && isDefined(elements.length) && !isWindow(elements)) - ? elements - : [ elements ]; - for(var i=0; i < elements.length; i++) { - root.push(elements[i]); + + // if a Node (the most common case) + if (elements.nodeType) { + root[root.length++] = elements; + } else { + var length = elements.length; + + // if an Array or NodeList and not a Window + if (typeof length === 'number' && elements.window !== elements) { + if (length) { + for (var i = 0; i < length; i++) { + root[root.length++] = elements[i]; + } + } + } else { + root[root.length++] = elements; + } } } } + function jqLiteController(element, name) { - return jqLiteInheritedData(element, '$' + (name || 'ngController' ) + 'Controller'); + return jqLiteInheritedData(element, '$' + (name || 'ngController') + 'Controller'); } function jqLiteInheritedData(element, name, value) { - element = jqLite(element); - // if element is the document object work with the html element instead // this makes $(document).scope() possible - if(element[0].nodeType == 9) { - element = element.find('html'); + if (element.nodeType === NODE_TYPE_DOCUMENT) { + element = element.documentElement; } var names = isArray(name) ? name : [name]; - while (element.length) { - + while (element) { for (var i = 0, ii = names.length; i < ii; i++) { - if ((value = element.data(names[i])) !== undefined) return value; + if (isDefined(value = jqLite.data(element, names[i]))) return value; } - element = element.parent(); + + // If dealing with a document fragment node with a host element, and no parent, use the host + // element as the parent. This enables directives within a Shadow DOM or polyfilled Shadow DOM + // to lookup parent controllers. + element = element.parentNode || (element.nodeType === NODE_TYPE_DOCUMENT_FRAGMENT && element.host); } } function jqLiteEmpty(element) { - for (var i = 0, childNodes = element.childNodes; i < childNodes.length; i++) { - jqLiteDealoc(childNodes[i]); - } + jqLiteDealoc(element, true); while (element.firstChild) { element.removeChild(element.firstChild); } } + function jqLiteRemove(element, keepData) { + if (!keepData) jqLiteDealoc(element); + var parent = element.parentNode; + if (parent) parent.removeChild(element); + } + + + function jqLiteDocumentLoaded(action, win) { + win = win || window; + if (win.document.readyState === 'complete') { + // Force the action to be run async for consistent behavior + // from the action's point of view + // i.e. it will definitely not be in a $apply + win.setTimeout(action); + } else { + // No need to unbind this handler as load is only ever called once + jqLite(win).on('load', action); + } + } + + function jqLiteReady(fn) { + function trigger() { + window.document.removeEventListener('DOMContentLoaded', trigger); + window.removeEventListener('load', trigger); + fn(); + } + + // check if document is already loaded + if (window.document.readyState === 'complete') { + window.setTimeout(fn); + } else { + // We can not use jqLite since we are not done loading and jQuery could be loaded later. + + // Works for modern browsers and IE9 + window.document.addEventListener('DOMContentLoaded', trigger); + + // Fallback to window.onload for others + window.addEventListener('load', trigger); + } + } + ////////////////////////////////////////// // Functions which are declared directly. ////////////////////////////////////////// var JQLitePrototype = JQLite.prototype = { - ready: function(fn) { - var fired = false; - - function trigger() { - if (fired) return; - fired = true; - fn(); - } - - // check if document already is loaded - if (document.readyState === 'complete'){ - setTimeout(trigger); - } else { - this.on('DOMContentLoaded', trigger); // works for modern browsers and IE9 - // we can not use jqLite since we are not done loading and jQuery could be loaded later. - // jshint -W064 - JQLite(window).on('load', trigger); // fallback to window.onload for others - // jshint +W064 - } - }, + ready: jqLiteReady, toString: function() { var value = []; - forEach(this, function(e){ value.push('' + e);}); + forEach(this, function(e) { value.push('' + e);}); return '[' + value.join(', ') + ']'; }, @@ -2379,92 +3514,106 @@ }); var BOOLEAN_ELEMENTS = {}; forEach('input,select,option,textarea,button,form,details'.split(','), function(value) { - BOOLEAN_ELEMENTS[uppercase(value)] = true; + BOOLEAN_ELEMENTS[value] = true; }); + var ALIASED_ATTR = { + 'ngMinlength': 'minlength', + 'ngMaxlength': 'maxlength', + 'ngMin': 'min', + 'ngMax': 'max', + 'ngPattern': 'pattern', + 'ngStep': 'step' + }; function getBooleanAttrName(element, name) { // check dom last since we will most likely fail on name var booleanAttr = BOOLEAN_ATTR[name.toLowerCase()]; // booleanAttr is here twice to minimize DOM access - return booleanAttr && BOOLEAN_ELEMENTS[element.nodeName] && booleanAttr; + return booleanAttr && BOOLEAN_ELEMENTS[nodeName_(element)] && booleanAttr; } + function getAliasedAttrName(name) { + return ALIASED_ATTR[name]; + } + + forEach({ + data: jqLiteData, + removeData: jqLiteRemoveData, + hasData: jqLiteHasData, + cleanData: function jqLiteCleanData(nodes) { + for (var i = 0, ii = nodes.length; i < ii; i++) { + jqLiteRemoveData(nodes[i]); + } + } + }, function(fn, name) { + JQLite[name] = fn; + }); + forEach({ data: jqLiteData, inheritedData: jqLiteInheritedData, scope: function(element) { // Can't use jqLiteData here directly so we stay compatible with jQuery! - return jqLite(element).data('$scope') || jqLiteInheritedData(element.parentNode || element, ['$isolateScope', '$scope']); + return jqLite.data(element, '$scope') || jqLiteInheritedData(element.parentNode || element, ['$isolateScope', '$scope']); }, isolateScope: function(element) { // Can't use jqLiteData here directly so we stay compatible with jQuery! - return jqLite(element).data('$isolateScope') || jqLite(element).data('$isolateScopeNoTemplate'); + return jqLite.data(element, '$isolateScope') || jqLite.data(element, '$isolateScopeNoTemplate'); }, - controller: jqLiteController , + controller: jqLiteController, injector: function(element) { return jqLiteInheritedData(element, '$injector'); }, - removeAttr: function(element,name) { + removeAttr: function(element, name) { element.removeAttribute(name); }, hasClass: jqLiteHasClass, css: function(element, name, value) { - name = camelCase(name); + name = cssKebabToCamel(name); if (isDefined(value)) { element.style[name] = value; } else { - var val; - - if (msie <= 8) { - // this is some IE specific weirdness that jQuery 1.6.4 does not sure why - val = element.currentStyle && element.currentStyle[name]; - if (val === '') val = 'auto'; - } - - val = val || element.style[name]; - - if (msie <= 8) { - // jquery weirdness :-/ - val = (val === '') ? undefined : val; - } - - return val; + return element.style[name]; } }, - attr: function(element, name, value){ + attr: function(element, name, value) { + var ret; + var nodeType = element.nodeType; + if (nodeType === NODE_TYPE_TEXT || nodeType === NODE_TYPE_ATTRIBUTE || nodeType === NODE_TYPE_COMMENT || + !element.getAttribute) { + return; + } + var lowercasedName = lowercase(name); - if (BOOLEAN_ATTR[lowercasedName]) { - if (isDefined(value)) { - if (!!value) { - element[name] = true; - element.setAttribute(name, lowercasedName); - } else { - element[name] = false; - element.removeAttribute(lowercasedName); - } + var isBooleanAttr = BOOLEAN_ATTR[lowercasedName]; + + if (isDefined(value)) { + // setter + + if (value === null || (value === false && isBooleanAttr)) { + element.removeAttribute(name); } else { - return (element[name] || - (element.attributes.getNamedItem(name)|| noop).specified) - ? lowercasedName - : undefined; + element.setAttribute(name, isBooleanAttr ? lowercasedName : value); } - } else if (isDefined(value)) { - element.setAttribute(name, value); - } else if (element.getAttribute) { - // the extra argument "2" is to get the right thing for a.href in IE, see jQuery code - // some elements (e.g. Document) don't have get attribute, so return undefined - var ret = element.getAttribute(name, 2); - // normalize non-existing attributes to undefined (as jQuery) + } else { + // getter + + ret = element.getAttribute(name); + + if (isBooleanAttr && ret !== null) { + ret = lowercasedName; + } + // Normalize non-existing attributes to undefined (as jQuery). return ret === null ? undefined : ret; } }, @@ -2478,36 +3627,28 @@ }, text: (function() { - var NODE_TYPE_TEXT_PROPERTY = []; - if (msie < 9) { - NODE_TYPE_TEXT_PROPERTY[1] = 'innerText'; /** Element **/ - NODE_TYPE_TEXT_PROPERTY[3] = 'nodeValue'; /** Text **/ - } else { - NODE_TYPE_TEXT_PROPERTY[1] = /** Element **/ - NODE_TYPE_TEXT_PROPERTY[3] = 'textContent'; /** Text **/ - } getText.$dv = ''; return getText; function getText(element, value) { - var textProp = NODE_TYPE_TEXT_PROPERTY[element.nodeType]; if (isUndefined(value)) { - return textProp ? element[textProp] : ''; + var nodeType = element.nodeType; + return (nodeType === NODE_TYPE_ELEMENT || nodeType === NODE_TYPE_TEXT) ? element.textContent : ''; } - element[textProp] = value; + element.textContent = value; } })(), val: function(element, value) { if (isUndefined(value)) { - if (nodeName_(element) === 'SELECT' && element.multiple) { + if (element.multiple && nodeName_(element) === 'select') { var result = []; - forEach(element.options, function (option) { + forEach(element.options, function(option) { if (option.selected) { result.push(option.value || option.text); } }); - return result.length === 0 ? null : result; + return result; } return element.value; } @@ -2518,29 +3659,28 @@ if (isUndefined(value)) { return element.innerHTML; } - for (var i = 0, childNodes = element.childNodes; i < childNodes.length; i++) { - jqLiteDealoc(childNodes[i]); - } + jqLiteDealoc(element, true); element.innerHTML = value; }, empty: jqLiteEmpty - }, function(fn, name){ + }, function(fn, name) { /** * Properties: writes return selection, reads return first value */ JQLite.prototype[name] = function(arg1, arg2) { var i, key; + var nodeCount = this.length; // jqLiteHasClass has only two arguments, but is a getter-only fn, so we need to special-case it // in a way that survives minification. // jqLiteEmpty takes no arguments but is a setter. if (fn !== jqLiteEmpty && - (((fn.length == 2 && (fn !== jqLiteHasClass && fn !== jqLiteController)) ? arg1 : arg2) === undefined)) { + (isUndefined((fn.length === 2 && (fn !== jqLiteHasClass && fn !== jqLiteController)) ? arg1 : arg2))) { if (isObject(arg1)) { // we are a write, but the object properties are the key/values - for (i = 0; i < this.length; i++) { + for (i = 0; i < nodeCount; i++) { if (fn === jqLiteData) { // data() takes the whole object in jQuery fn(this[i], arg1); @@ -2554,9 +3694,10 @@ return this; } else { // we are a read, so read the first child. + // TODO: do we still need this? var value = fn.$dv; // Only if we have $dv do we iterate over all, otherwise it is just the first element. - var jj = (value === undefined) ? Math.min(this.length, 1) : this.length; + var jj = (isUndefined(value)) ? Math.min(nodeCount, 1) : nodeCount; for (var j = 0; j < jj; j++) { var nodeValue = fn(this[j], arg1, arg2); value = value ? value + nodeValue : nodeValue; @@ -2565,7 +3706,7 @@ } } else { // we are a write, so apply to all children - for (i = 0; i < this.length; i++) { + for (i = 0; i < nodeCount; i++) { fn(this[i], arg1, arg2); } // return self for chaining @@ -2575,61 +3716,73 @@ }); function createEventHandler(element, events) { - var eventHandler = function (event, type) { - if (!event.preventDefault) { - event.preventDefault = function() { - event.returnValue = false; //ie - }; - } - - if (!event.stopPropagation) { - event.stopPropagation = function() { - event.cancelBubble = true; //ie - }; - } - - if (!event.target) { - event.target = event.srcElement || document; - } - - if (isUndefined(event.defaultPrevented)) { - var prevent = event.preventDefault; - event.preventDefault = function() { - event.defaultPrevented = true; - prevent.call(event); - }; - event.defaultPrevented = false; - } - + var eventHandler = function(event, type) { + // jQuery specific api event.isDefaultPrevented = function() { - return event.defaultPrevented || event.returnValue === false; + return event.defaultPrevented; }; + var eventFns = events[type || event.type]; + var eventFnsLength = eventFns ? eventFns.length : 0; + + if (!eventFnsLength) return; + + if (isUndefined(event.immediatePropagationStopped)) { + var originalStopImmediatePropagation = event.stopImmediatePropagation; + event.stopImmediatePropagation = function() { + event.immediatePropagationStopped = true; + + if (event.stopPropagation) { + event.stopPropagation(); + } + + if (originalStopImmediatePropagation) { + originalStopImmediatePropagation.call(event); + } + }; + } + + event.isImmediatePropagationStopped = function() { + return event.immediatePropagationStopped === true; + }; + + // Some events have special handlers that wrap the real handler + var handlerWrapper = eventFns.specialHandlerWrapper || defaultHandlerWrapper; + // Copy event handlers in case event handlers array is modified during execution. - var eventHandlersCopy = shallowCopy(events[type || event.type] || []); + if ((eventFnsLength > 1)) { + eventFns = shallowCopy(eventFns); + } - forEach(eventHandlersCopy, function(fn) { - fn.call(element, event); - }); - - // Remove monkey-patched methods (IE), - // as they would cause memory leaks in IE8. - if (msie <= 8) { - // IE7/8 does not allow to delete property on native object - event.preventDefault = null; - event.stopPropagation = null; - event.isDefaultPrevented = null; - } else { - // It shouldn't affect normal browsers (native methods are defined on prototype). - delete event.preventDefault; - delete event.stopPropagation; - delete event.isDefaultPrevented; + for (var i = 0; i < eventFnsLength; i++) { + if (!event.isImmediatePropagationStopped()) { + handlerWrapper(element, event, eventFns[i]); + } } }; + + // TODO: this is a hack for angularMocks/clearDataCache that makes it possible to deregister all + // events on `element` eventHandler.elem = element; return eventHandler; } + function defaultHandlerWrapper(element, event, handler) { + handler.call(element, event); + } + + function specialMouseHandlerWrapper(target, event, handler) { + // Refer to jQuery's implementation of mouseenter & mouseleave + // Read about mouseenter and mouseleave: + // http://www.quirksmode.org/js/events_mouse.html#link8 + var related = event.relatedTarget; + // For mousenter/leave call the handler if related is outside the target. + // NB: No relatedTarget if the mouse left/entered the browser window + if (!related || (related !== target && !jqLiteContains.call(target, related))) { + handler.call(target, event); + } + } + ////////////////////////////////////////// // Functions iterating traversal. // These functions chain results into a single @@ -2638,68 +3791,49 @@ forEach({ removeData: jqLiteRemoveData, - dealoc: jqLiteDealoc, - - on: function onFn(element, type, fn, unsupported){ + on: function jqLiteOn(element, type, fn, unsupported) { if (isDefined(unsupported)) throw jqLiteMinErr('onargs', 'jqLite#on() does not support the `selector` or `eventData` parameters'); - var events = jqLiteExpandoStore(element, 'events'), - handle = jqLiteExpandoStore(element, 'handle'); + // Do not add event handlers to non-elements because they will not be cleaned up. + if (!jqLiteAcceptsData(element)) { + return; + } - if (!events) jqLiteExpandoStore(element, 'events', events = {}); - if (!handle) jqLiteExpandoStore(element, 'handle', handle = createEventHandler(element, events)); + var expandoStore = jqLiteExpandoStore(element, true); + var events = expandoStore.events; + var handle = expandoStore.handle; - forEach(type.split(' '), function(type){ + if (!handle) { + handle = expandoStore.handle = createEventHandler(element, events); + } + + // http://jsperf.com/string-indexof-vs-split + var types = type.indexOf(' ') >= 0 ? type.split(' ') : [type]; + var i = types.length; + + var addHandler = function(type, specialHandlerWrapper, noEventListener) { var eventFns = events[type]; if (!eventFns) { - if (type == 'mouseenter' || type == 'mouseleave') { - var contains = document.body.contains || document.body.compareDocumentPosition ? - function( a, b ) { - // jshint bitwise: false - var adown = a.nodeType === 9 ? a.documentElement : a, - bup = b && b.parentNode; - return a === bup || !!( bup && bup.nodeType === 1 && ( - adown.contains ? - adown.contains( bup ) : - a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 - )); - } : - function( a, b ) { - if ( b ) { - while ( (b = b.parentNode) ) { - if ( b === a ) { - return true; - } - } - } - return false; - }; - - events[type] = []; - - // Refer to jQuery's implementation of mouseenter & mouseleave - // Read about mouseenter and mouseleave: - // http://www.quirksmode.org/js/events_mouse.html#link8 - var eventmap = { mouseleave : "mouseout", mouseenter : "mouseover"}; - - onFn(element, eventmap[type], function(event) { - var target = this, related = event.relatedTarget; - // For mousenter/leave call the handler if related is outside the target. - // NB: No relatedTarget if the mouse left/entered the browser window - if ( !related || (related !== target && !contains(target, related)) ){ - handle(event, type); - } - }); - - } else { - addEventListenerFn(element, type, handle); - events[type] = []; + eventFns = events[type] = []; + eventFns.specialHandlerWrapper = specialHandlerWrapper; + if (type !== '$destroy' && !noEventListener) { + element.addEventListener(type, handle); } - eventFns = events[type]; } + eventFns.push(fn); - }); + }; + + while (i--) { + type = types[i]; + if (MOUSE_EVENT_MAP[type]) { + addHandler(MOUSE_EVENT_MAP[type], specialMouseHandlerWrapper); + addHandler(type, undefined, true); + } else { + addHandler(type); + } + } }, off: jqLiteOff, @@ -2720,7 +3854,7 @@ replaceWith: function(element, replaceNode) { var index, parent = element.parentNode; jqLiteDealoc(element); - forEach(new JQLite(replaceNode), function(node){ + forEach(new JQLite(replaceNode), function(node) { if (index) { parent.insertBefore(node, index.nextSibling); } else { @@ -2732,83 +3866,85 @@ children: function(element) { var children = []; - forEach(element.childNodes, function(element){ - if (element.nodeType === 1) + forEach(element.childNodes, function(element) { + if (element.nodeType === NODE_TYPE_ELEMENT) { children.push(element); + } }); return children; }, contents: function(element) { - return element.childNodes || []; + return element.contentDocument || element.childNodes || []; }, append: function(element, node) { - forEach(new JQLite(node), function(child){ - if (element.nodeType === 1 || element.nodeType === 11) { - element.appendChild(child); - } - }); + var nodeType = element.nodeType; + if (nodeType !== NODE_TYPE_ELEMENT && nodeType !== NODE_TYPE_DOCUMENT_FRAGMENT) return; + + node = new JQLite(node); + + for (var i = 0, ii = node.length; i < ii; i++) { + var child = node[i]; + element.appendChild(child); + } }, prepend: function(element, node) { - if (element.nodeType === 1) { + if (element.nodeType === NODE_TYPE_ELEMENT) { var index = element.firstChild; - forEach(new JQLite(node), function(child){ + forEach(new JQLite(node), function(child) { element.insertBefore(child, index); }); } }, wrap: function(element, wrapNode) { - wrapNode = jqLite(wrapNode)[0]; - var parent = element.parentNode; - if (parent) { - parent.replaceChild(wrapNode, element); - } - wrapNode.appendChild(element); + jqLiteWrapNode(element, jqLite(wrapNode).eq(0).clone()[0]); }, - remove: function(element) { - jqLiteDealoc(element); - var parent = element.parentNode; - if (parent) parent.removeChild(element); + remove: jqLiteRemove, + + detach: function(element) { + jqLiteRemove(element, true); }, after: function(element, newElement) { var index = element, parent = element.parentNode; - forEach(new JQLite(newElement), function(node){ - parent.insertBefore(node, index.nextSibling); - index = node; - }); + + if (parent) { + newElement = new JQLite(newElement); + + for (var i = 0, ii = newElement.length; i < ii; i++) { + var node = newElement[i]; + parent.insertBefore(node, index.nextSibling); + index = node; + } + } }, addClass: jqLiteAddClass, removeClass: jqLiteRemoveClass, toggleClass: function(element, selector, condition) { - if (isUndefined(condition)) { - condition = !jqLiteHasClass(element, selector); + if (selector) { + forEach(selector.split(' '), function(className) { + var classCondition = condition; + if (isUndefined(classCondition)) { + classCondition = !jqLiteHasClass(element, className); + } + (classCondition ? jqLiteAddClass : jqLiteRemoveClass)(element, className); + }); } - (condition ? jqLiteAddClass : jqLiteRemoveClass)(element, selector); }, parent: function(element) { var parent = element.parentNode; - return parent && parent.nodeType !== 11 ? parent : null; + return parent && parent.nodeType !== NODE_TYPE_DOCUMENT_FRAGMENT ? parent : null; }, next: function(element) { - if (element.nextElementSibling) { - return element.nextElementSibling; - } - - // IE8 doesn't have nextElementSibling - var elm = element.nextSibling; - while (elm != null && elm.nodeType !== 1) { - elm = elm.nextSibling; - } - return elm; + return element.nextElementSibling; }, find: function(element, selector) { @@ -2821,27 +3957,50 @@ clone: jqLiteClone, - triggerHandler: function(element, eventName, eventData) { - var eventFns = (jqLiteExpandoStore(element, 'events') || {})[eventName]; + triggerHandler: function(element, event, extraParameters) { - eventData = eventData || []; + var dummyEvent, eventFnsCopy, handlerArgs; + var eventName = event.type || event; + var expandoStore = jqLiteExpandoStore(element); + var events = expandoStore && expandoStore.events; + var eventFns = events && events[eventName]; - var event = [{ - preventDefault: noop, - stopPropagation: noop - }]; + if (eventFns) { + // Create a dummy event to pass to the handlers + dummyEvent = { + preventDefault: function() { this.defaultPrevented = true; }, + isDefaultPrevented: function() { return this.defaultPrevented === true; }, + stopImmediatePropagation: function() { this.immediatePropagationStopped = true; }, + isImmediatePropagationStopped: function() { return this.immediatePropagationStopped === true; }, + stopPropagation: noop, + type: eventName, + target: element + }; - forEach(eventFns, function(fn) { - fn.apply(element, event.concat(eventData)); - }); + // If a custom event was provided then extend our dummy event with it + if (event.type) { + dummyEvent = extend(dummyEvent, event); + } + + // Copy event handlers in case event handlers array is modified during execution. + eventFnsCopy = shallowCopy(eventFns); + handlerArgs = extraParameters ? [dummyEvent].concat(extraParameters) : [dummyEvent]; + + forEach(eventFnsCopy, function(fn) { + if (!dummyEvent.isImmediatePropagationStopped()) { + fn.apply(element, handlerArgs); + } + }); + } } - }, function(fn, name){ + }, function(fn, name) { /** * chaining functions */ JQLite.prototype[name] = function(arg1, arg2, arg3) { var value; - for(var i=0; i < this.length; i++) { + + for (var i = 0, ii = this.length; i < ii; i++) { if (isUndefined(value)) { value = fn(this[i], arg1, arg2, arg3); if (isDefined(value)) { @@ -2854,12 +4013,34 @@ } return isDefined(value) ? value : this; }; - - // bind legacy bind/unbind to on/off - JQLite.prototype.bind = JQLite.prototype.on; - JQLite.prototype.unbind = JQLite.prototype.off; }); +// bind legacy bind/unbind to on/off + JQLite.prototype.bind = JQLite.prototype.on; + JQLite.prototype.unbind = JQLite.prototype.off; + + +// Provider for private $$jqLite service + /** @this */ + function $$jqLiteProvider() { + this.$get = function $$jqLite() { + return extend(JQLite, { + hasClass: function(node, classes) { + if (node.attr) node = node[0]; + return jqLiteHasClass(node, classes); + }, + addClass: function(node, classes) { + if (node.attr) node = node[0]; + return jqLiteAddClass(node, classes); + }, + removeClass: function(node, classes) { + if (node.attr) node = node[0]; + return jqLiteRemoveClass(node, classes); + } + }); + }; + } + /** * Computes a hash of an 'obj'. * Hash of a: @@ -2872,90 +4053,126 @@ * @returns {string} hash string such that the same input will have the same hash string. * The resulting string key is in 'type:hashKey' format. */ - function hashKey(obj) { - var objType = typeof obj, - key; + function hashKey(obj, nextUidFn) { + var key = obj && obj.$$hashKey; - if (objType == 'object' && obj !== null) { - if (typeof (key = obj.$$hashKey) == 'function') { - // must invoke on object to keep the right this + if (key) { + if (typeof key === 'function') { key = obj.$$hashKey(); - } else if (key === undefined) { - key = obj.$$hashKey = nextUid(); } - } else { - key = obj; + return key; } - return objType + ':' + key; + var objType = typeof obj; + if (objType === 'function' || (objType === 'object' && obj !== null)) { + key = obj.$$hashKey = objType + ':' + (nextUidFn || nextUid)(); + } else { + key = objType + ':' + obj; + } + + return key; } - /** - * HashMap which can use objects as keys - */ - function HashMap(array){ - forEach(array, this.put, this); +// A minimal ES2015 Map implementation. +// Should be bug/feature equivalent to the native implementations of supported browsers +// (for the features required in Angular). +// See https://kangax.github.io/compat-table/es6/#test-Map + var nanKey = Object.create(null); + function NgMapShim() { + this._keys = []; + this._values = []; + this._lastKey = NaN; + this._lastIndex = -1; } - HashMap.prototype = { - /** - * Store key value pair - * @param key key to store can be any type - * @param value value to store can be any type - */ - put: function(key, value) { - this[hashKey(key)] = value; + NgMapShim.prototype = { + _idx: function(key) { + if (key === this._lastKey) { + return this._lastIndex; + } + this._lastKey = key; + this._lastIndex = this._keys.indexOf(key); + return this._lastIndex; + }, + _transformKey: function(key) { + return isNumberNaN(key) ? nanKey : key; }, - - /** - * @param key - * @returns the value for the key - */ get: function(key) { - return this[hashKey(key)]; + key = this._transformKey(key); + var idx = this._idx(key); + if (idx !== -1) { + return this._values[idx]; + } }, + set: function(key, value) { + key = this._transformKey(key); + var idx = this._idx(key); + if (idx === -1) { + idx = this._lastIndex = this._keys.length; + } + this._keys[idx] = key; + this._values[idx] = value; - /** - * Remove the key/value pair - * @param key - */ - remove: function(key) { - var value = this[key = hashKey(key)]; - delete this[key]; - return value; + // Support: IE11 + // Do not `return this` to simulate the partial IE11 implementation + }, + delete: function(key) { + key = this._transformKey(key); + var idx = this._idx(key); + if (idx === -1) { + return false; + } + this._keys.splice(idx, 1); + this._values.splice(idx, 1); + this._lastKey = NaN; + this._lastIndex = -1; + return true; } }; +// For now, always use `NgMapShim`, even if `window.Map` is available. Some native implementations +// are still buggy (often in subtle ways) and can cause hard-to-debug failures. When native `Map` +// implementations get more stable, we can reconsider switching to `window.Map` (when available). + var NgMap = NgMapShim; + + var $$MapProvider = [/** @this */function() { + this.$get = [function() { + return NgMap; + }]; + }]; + /** * @ngdoc function + * @module ng * @name angular.injector - * @function + * @kind function * * @description - * Creates an injector function that can be used for retrieving services as well as for + * Creates an injector object that can be used for retrieving services as well as for * dependency injection (see {@link guide/di dependency injection}). * - * @param {Array.} modules A list of module functions or their aliases. See - * {@link angular.module}. The `ng` module must be explicitly added. - * @returns {function()} Injector function. See {@link AUTO.$injector $injector}. + * {@link angular.module}. The `ng` module must be explicitly added. + * @param {boolean=} [strictDi=false] Whether the injector should be in strict mode, which + * disallows argument name annotation inference. + * @returns {injector} Injector object. See {@link auto.$injector $injector}. * * @example * Typical usage - *
    +   * ```js
        *   // create an injector
        *   var $injector = angular.injector(['ng']);
        *
        *   // use the injector to kick off your application
        *   // use the type inference to auto inject arguments, or use implicit injection
    -   *   $injector.invoke(function($rootScope, $compile, $document){
    +   *   $injector.invoke(function($rootScope, $compile, $document) {
      *     $compile($document)($rootScope);
      *     $rootScope.$digest();
      *   });
    -   * 
    + * ``` * * Sometimes you want to get access to the injector of a currently running Angular app * from outside Angular. Perhaps, you want to inject and compile some markup after the - * application has been bootstrapped. You can do this using extra `injector()` added + * application has been bootstrapped. You can do this using the extra `injector()` added * to JQuery/jqLite elements. See {@link angular.element}. * * *This is fairly rare but could be the case if a third party library is injecting the @@ -2965,7 +4182,7 @@ * directive is added to the end of the document body by JQuery. We then compile and link * it into the current AngularJS scope. * - *
    +   * ```js
        * var $div = $('
    {{content.label}}
    '); * $(document.body).append($div); * @@ -2973,37 +4190,65 @@ * var scope = angular.element($div).scope(); * $compile($div)(scope); * }); - *
    + * ``` */ /** - * @ngdoc overview - * @name AUTO + * @ngdoc module + * @name auto + * @installation * @description * - * Implicit module which gets automatically added to each {@link AUTO.$injector $injector}. + * Implicit module which gets automatically added to each {@link auto.$injector $injector}. */ - var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m; + var ARROW_ARG = /^([^(]+?)=>/; + var FN_ARGS = /^[^(]*\(\s*([^)]*)\)/m; var FN_ARG_SPLIT = /,/; var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/; var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg; var $injectorMinErr = minErr('$injector'); - function annotate(fn) { - var $inject, - fnText, - argDecl, - last; - if (typeof fn == 'function') { + function stringifyFn(fn) { + return Function.prototype.toString.call(fn); + } + + function extractArgs(fn) { + var fnText = stringifyFn(fn).replace(STRIP_COMMENTS, ''), + args = fnText.match(ARROW_ARG) || fnText.match(FN_ARGS); + return args; + } + + function anonFn(fn) { + // For anonymous functions, showing at the very least the function signature can help in + // debugging. + var args = extractArgs(fn); + if (args) { + return 'function(' + (args[1] || '').replace(/[\s\r\n]+/, ' ') + ')'; + } + return 'fn'; + } + + function annotate(fn, strictDi, name) { + var $inject, + argDecl, + last; + + if (typeof fn === 'function') { if (!($inject = fn.$inject)) { $inject = []; if (fn.length) { - fnText = fn.toString().replace(STRIP_COMMENTS, ''); - argDecl = fnText.match(FN_ARGS); - forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg){ - arg.replace(FN_ARG, function(all, underscore, name){ + if (strictDi) { + if (!isString(name) || !name) { + name = fn.name || anonFn(fn); + } + throw $injectorMinErr('strictdi', + '{0} is not using explicit annotation and cannot be invoked in strict mode', name); + } + argDecl = extractArgs(fn); + forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg) { + arg.replace(FN_ARG, function(all, underscore, name) { $inject.push(name); }); }); @@ -3023,32 +4268,31 @@ /////////////////////////////////////// /** - * @ngdoc object - * @name AUTO.$injector - * @function + * @ngdoc service + * @name $injector * * @description * * `$injector` is used to retrieve object instances as defined by - * {@link AUTO.$provide provider}, instantiate types, invoke methods, + * {@link auto.$provide provider}, instantiate types, invoke methods, * and load modules. * * The following always holds true: * - *
    +   * ```js
        *   var $injector = angular.injector();
        *   expect($injector.get('$injector')).toBe($injector);
    -   *   expect($injector.invoke(function($injector){
    +   *   expect($injector.invoke(function($injector) {
      *     return $injector;
    - *   }).toBe($injector);
    -   * 
    + * })).toBe($injector); + * ``` * * # Injection Function Annotation * * JavaScript does not have annotations, and annotations are needed for dependency injection. The * following are all valid ways of annotating function with injection arguments and are equivalent. * - *
    +   * ```js
        *   // inferred (only works if code not minified/obfuscated)
        *   $injector.invoke(function(serviceA){});
        *
    @@ -3059,43 +4303,66 @@
        *
        *   // inline
        *   $injector.invoke(['serviceA', function(serviceA){}]);
    -   * 
    + * ``` * * ## Inference * * In JavaScript calling `toString()` on a function returns the function definition. The definition - * can then be parsed and the function arguments can be extracted. *NOTE:* This does not work with - * minification, and obfuscation tools since these tools change the argument names. + * can then be parsed and the function arguments can be extracted. This method of discovering + * annotations is disallowed when the injector is in strict mode. + * *NOTE:* This does not work with minification, and obfuscation tools since these tools change the + * argument names. * * ## `$inject` Annotation - * By adding a `$inject` property onto a function the injection parameters can be specified. + * By adding an `$inject` property onto a function the injection parameters can be specified. * * ## Inline * As an array of injection names, where the last item in the array is the function to call. */ + /** + * @ngdoc property + * @name $injector#modules + * @type {Object} + * @description + * A hash containing all the modules that have been loaded into the + * $injector. + * + * You can use this property to find out information about a module via the + * {@link angular.Module#info `myModule.info(...)`} method. + * + * For example: + * + * ``` + * var info = $injector.modules['ngAnimate'].info(); + * ``` + * + * **Do not use this property to attempt to modify the modules after the application + * has been bootstrapped.** + */ + + /** * @ngdoc method - * @name AUTO.$injector#get - * @methodOf AUTO.$injector + * @name $injector#get * * @description * Return an instance of the service. * * @param {string} name The name of the instance to retrieve. + * @param {string=} caller An optional string to provide the origin of the function call for error messages. * @return {*} The instance. */ /** * @ngdoc method - * @name AUTO.$injector#invoke - * @methodOf AUTO.$injector + * @name $injector#invoke * * @description * Invoke the method and supply the method arguments from the `$injector`. * - * @param {!function} fn The function to invoke. Function parameters are injected according to the - * {@link guide/di $inject Annotation} rules. + * @param {Function|Array.} fn The injectable function to invoke. Function parameters are + * injected according to the {@link guide/di $inject Annotation} rules. * @param {Object=} self The `this` for the invoked method. * @param {Object=} locals Optional object. If preset then any argument names are read from this * object first, before the `$injector` is consulted. @@ -3104,26 +4371,24 @@ /** * @ngdoc method - * @name AUTO.$injector#has - * @methodOf AUTO.$injector + * @name $injector#has * * @description - * Allows the user to query if the particular service exist. + * Allows the user to query if the particular service exists. * - * @param {string} Name of the service to query. - * @returns {boolean} returns true if injector has given service. + * @param {string} name Name of the service to query. + * @returns {boolean} `true` if injector has given service. */ /** * @ngdoc method - * @name AUTO.$injector#instantiate - * @methodOf AUTO.$injector + * @name $injector#instantiate * @description - * Create a new instance of JS type. The method takes a constructor function invokes the new - * operator and supplies all of the arguments to the constructor function as specified by the + * Create a new instance of JS type. The method takes a constructor function, invokes the new + * operator, and supplies all of the arguments to the constructor function as specified by the * constructor annotation. * - * @param {function} Type Annotated constructor function. + * @param {Function} Type Annotated constructor function. * @param {Object=} locals Optional object. If preset then any argument names are read from this * object first, before the `$injector` is consulted. * @returns {Object} new instance of `Type`. @@ -3131,8 +4396,7 @@ /** * @ngdoc method - * @name AUTO.$injector#annotate - * @methodOf AUTO.$injector + * @name $injector#annotate * * @description * Returns an array of service names which the function is requesting for injection. This API is @@ -3145,7 +4409,7 @@ * The simplest form is to extract the dependencies from the arguments of the function. This is done * by converting the function into a string using `toString()` method and extracting the argument * names. - *
    +   * ```js
        *   // Given
        *   function MyController($scope, $route) {
      *     // ...
    @@ -3153,7 +4417,9 @@
        *
        *   // Then
        *   expect(injector.annotate(MyController)).toEqual(['$scope', '$route']);
    -   * 
    + * ``` + * + * You can disallow this method by using strict injection mode. * * This method does not work with code minification / obfuscation. For this reason the following * annotation strategies are supported. @@ -3162,7 +4428,7 @@ * * If a function has an `$inject` property and its value is an array of strings, then the strings * represent names of services to be injected into the function. - *
    +   * ```js
        *   // Given
        *   var MyController = function(obfuscatedScope, obfuscatedRoute) {
      *     // ...
    @@ -3172,7 +4438,7 @@
        *
        *   // Then
        *   expect(injector.annotate(MyController)).toEqual(['$scope', '$route']);
    -   * 
    + * ``` * * # The array notation * @@ -3180,7 +4446,7 @@ * is very inconvenient. In these situations using the array notation to specify the dependencies in * a way that survives minification is a better choice: * - *
    +   * ```js
        *   // We wish to write this (not minification / obfuscation safe)
        *   injector.invoke(function($compile, $rootScope) {
      *     // ...
    @@ -3202,25 +4468,26 @@
        *   expect(injector.annotate(
        *      ['$compile', '$rootScope', function(obfus_$compile, obfus_$rootScope) {}])
        *    ).toEqual(['$compile', '$rootScope']);
    -   * 
    + * ``` * - * @param {function|Array.} fn Function for which dependent service names need to + * @param {Function|Array.} fn Function for which dependent service names need to * be retrieved as described above. * + * @param {boolean=} [strictDi=false] Disallow argument name annotation inference. + * * @returns {Array.} The names of the services which the function requires. */ - /** - * @ngdoc object - * @name AUTO.$provide + * @ngdoc service + * @name $provide * * @description * - * The {@link AUTO.$provide $provide} service has a number of methods for registering components - * with the {@link AUTO.$injector $injector}. Many of these functions are also exposed on + * The {@link auto.$provide $provide} service has a number of methods for registering components + * with the {@link auto.$injector $injector}. Many of these functions are also exposed on * {@link angular.Module}. * * An Angular **service** is a singleton object created by a **service factory**. These **service @@ -3228,38 +4495,39 @@ * The **service providers** are constructor functions. When instantiated they must contain a * property called `$get`, which holds the **service factory** function. * - * When you request a service, the {@link AUTO.$injector $injector} is responsible for finding the + * When you request a service, the {@link auto.$injector $injector} is responsible for finding the * correct **service provider**, instantiating it and then calling its `$get` **service factory** * function to get the instance of the **service**. * * Often services have no configuration options and there is no need to add methods to the service * provider. The provider will be no more than a constructor function with a `$get` property. For - * these cases the {@link AUTO.$provide $provide} service has additional helper methods to register + * these cases the {@link auto.$provide $provide} service has additional helper methods to register * services without specifying a provider. * - * * {@link AUTO.$provide#methods_provider provider(provider)} - registers a **service provider** with the - * {@link AUTO.$injector $injector} - * * {@link AUTO.$provide#methods_constant constant(obj)} - registers a value/object that can be accessed by + * * {@link auto.$provide#provider provider(name, provider)} - registers a **service provider** with the + * {@link auto.$injector $injector} + * * {@link auto.$provide#constant constant(name, obj)} - registers a value/object that can be accessed by * providers and services. - * * {@link AUTO.$provide#methods_value value(obj)} - registers a value/object that can only be accessed by + * * {@link auto.$provide#value value(name, obj)} - registers a value/object that can only be accessed by * services, not providers. - * * {@link AUTO.$provide#methods_factory factory(fn)} - registers a service **factory function**, `fn`, + * * {@link auto.$provide#factory factory(name, fn)} - registers a service **factory function** * that will be wrapped in a **service provider** object, whose `$get` property will contain the * given factory function. - * * {@link AUTO.$provide#methods_service service(class)} - registers a **constructor function**, `class` that + * * {@link auto.$provide#service service(name, Fn)} - registers a **constructor function** * that will be wrapped in a **service provider** object, whose `$get` property will instantiate * a new object using the given constructor function. + * * {@link auto.$provide#decorator decorator(name, decorFn)} - registers a **decorator function** that + * will be able to modify or replace the implementation of another service. * * See the individual methods for more information and examples. */ /** * @ngdoc method - * @name AUTO.$provide#provider - * @methodOf AUTO.$provide + * @name $provide#provider * @description * - * Register a **provider function** with the {@link AUTO.$injector $injector}. Provider functions + * Register a **provider function** with the {@link auto.$injector $injector}. Provider functions * are constructor functions, whose instances are responsible for "providing" a factory for a * service. * @@ -3279,20 +4547,18 @@ * @param {(Object|function())} provider If the provider is: * * - `Object`: then it should have a `$get` method. The `$get` method will be invoked using - * {@link AUTO.$injector#invoke $injector.invoke()} when an instance needs to be - * created. + * {@link auto.$injector#invoke $injector.invoke()} when an instance needs to be created. * - `Constructor`: a new instance of the provider will be created using - * {@link AUTO.$injector#instantiate $injector.instantiate()}, then treated as - * `object`. + * {@link auto.$injector#instantiate $injector.instantiate()}, then treated as `object`. * * @returns {Object} registered provider instance * @example * * The following example shows how to create a simple event tracking service and register it using - * {@link AUTO.$provide#methods_provider $provide.provider()}. + * {@link auto.$provide#provider $provide.provider()}. * - *
    +   * ```js
        *  // Define the eventTracker provider
        *  function EventTrackerProvider() {
      *    var trackingUrl = '/track';
    @@ -3349,97 +4615,110 @@
      *      expect(postSpy.mostRecentCall.args[1]).toEqual({ 'login': 1 });
      *    }));
      *  });
    -   * 
    + * ``` */ /** * @ngdoc method - * @name AUTO.$provide#factory - * @methodOf AUTO.$provide + * @name $provide#factory * @description * * Register a **service factory**, which will be called to return the service instance. * This is short for registering a service where its provider consists of only a `$get` property, * which is the given service factory function. - * You should use {@link AUTO.$provide#factory $provide.factory(getFn)} if you do not need to + * You should use {@link auto.$provide#factory $provide.factory(getFn)} if you do not need to * configure your service in a provider. * * @param {string} name The name of the instance. - * @param {function()} $getFn The $getFn for the instance creation. Internally this is a short hand - * for `$provide.provider(name, {$get: $getFn})`. + * @param {Function|Array.} $getFn The injectable $getFn for the instance creation. + * Internally this is a short hand for `$provide.provider(name, {$get: $getFn})`. * @returns {Object} registered provider instance * * @example * Here is an example of registering a service - *
    +   * ```js
        *   $provide.factory('ping', ['$http', function($http) {
      *     return function ping() {
      *       return $http.send('/ping');
      *     };
      *   }]);
    -   * 
    + * ``` * You would then inject and use this service like this: - *
    +   * ```js
        *   someModule.controller('Ctrl', ['ping', function(ping) {
      *     ping();
      *   }]);
    -   * 
    + * ``` */ /** * @ngdoc method - * @name AUTO.$provide#service - * @methodOf AUTO.$provide + * @name $provide#service * @description * * Register a **service constructor**, which will be invoked with `new` to create the service * instance. - * This is short for registering a service where its provider's `$get` property is the service - * constructor function that will be used to instantiate the service instance. + * This is short for registering a service where its provider's `$get` property is a factory + * function that returns an instance instantiated by the injector from the service constructor + * function. * - * You should use {@link AUTO.$provide#methods_service $provide.service(class)} if you define your service - * as a type/class. This is common when using {@link http://coffeescript.org CoffeeScript}. + * Internally it looks a bit like this: + * + * ``` + * { + * $get: function() { + * return $injector.instantiate(constructor); + * } + * } + * ``` + * + * + * You should use {@link auto.$provide#service $provide.service(class)} if you define your service + * as a type/class. * * @param {string} name The name of the instance. - * @param {Function} constructor A class (constructor function) that will be instantiated. + * @param {Function|Array.} constructor An injectable class (constructor function) + * that will be instantiated. * @returns {Object} registered provider instance * * @example * Here is an example of registering a service using - * {@link AUTO.$provide#methods_service $provide.service(class)} that is defined as a CoffeeScript class. - *
    -   *   class Ping
    -   *     constructor: (@$http) ->
    -   *     send: () =>
    -   *       @$http.get('/ping')
    +   * {@link auto.$provide#service $provide.service(class)}.
    +   * ```js
    +   *   var Ping = function($http) {
    + *     this.$http = $http;
    + *   };
        *
    -   *   $provide.service('ping', ['$http', Ping])
    -   * 
    + * Ping.$inject = ['$http']; + * + * Ping.prototype.send = function() { + * return this.$http.get('/ping'); + * }; + * $provide.service('ping', Ping); + * ``` * You would then inject and use this service like this: - *
    -   *   someModule.controller 'Ctrl', ['ping', (ping) ->
    -   *     ping.send()
    -   *   ]
    -   * 
    + * ```js + * someModule.controller('Ctrl', ['ping', function(ping) { + * ping.send(); + * }]); + * ``` */ /** * @ngdoc method - * @name AUTO.$provide#value - * @methodOf AUTO.$provide + * @name $provide#value * @description * - * Register a **value service** with the {@link AUTO.$injector $injector}, such as a string, a - * number, an array, an object or a function. This is short for registering a service where its + * Register a **value service** with the {@link auto.$injector $injector}, such as a string, a + * number, an array, an object or a function. This is short for registering a service where its * provider's `$get` property is a factory function that takes no arguments and returns the **value - * service**. + * service**. That also means it is not possible to inject other services into a value service. * * Value services are similar to constant services, except that they cannot be injected into a * module configuration function (see {@link angular.Module#config}) but they can be overridden by - * an Angular - * {@link AUTO.$provide#decorator decorator}. + * an Angular {@link auto.$provide#decorator decorator}. * * @param {string} name The name of the instance. * @param {*} value The value. @@ -3447,7 +4726,7 @@ * * @example * Here are some examples of creating value services. - *
    +   * ```js
        *   $provide.value('ADMIN_USER', 'admin');
        *
        *   $provide.value('RoleLookup', { admin: 0, writer: 1, reader: 2 });
    @@ -3455,20 +4734,22 @@
        *   $provide.value('halfOf', function(value) {
      *     return value / 2;
      *   });
    -   * 
    + * ``` */ /** * @ngdoc method - * @name AUTO.$provide#constant - * @methodOf AUTO.$provide + * @name $provide#constant * @description * - * Register a **constant service**, such as a string, a number, an array, an object or a function, - * with the {@link AUTO.$injector $injector}. Unlike {@link AUTO.$provide#value value} it can be + * Register a **constant service** with the {@link auto.$injector $injector}, such as a string, + * a number, an array, an object or a function. Like the {@link auto.$provide#value value}, it is not + * possible to inject other services into a constant. + * + * But unlike {@link auto.$provide#value value}, a constant can be * injected into a module configuration function (see {@link angular.Module#config}) and it cannot - * be overridden by an Angular {@link AUTO.$provide#decorator decorator}. + * be overridden by an Angular {@link auto.$provide#decorator decorator}. * * @param {string} name The name of the constant. * @param {*} value The constant value. @@ -3476,7 +4757,7 @@ * * @example * Here a some examples of creating constants: - *
    +   * ```js
        *   $provide.constant('SHARD_HEIGHT', 306);
        *
        *   $provide.constant('MY_COLOURS', ['red', 'blue', 'grey']);
    @@ -3484,70 +4765,81 @@
        *   $provide.constant('double', function(value) {
      *     return value * 2;
      *   });
    -   * 
    + * ``` */ /** * @ngdoc method - * @name AUTO.$provide#decorator - * @methodOf AUTO.$provide + * @name $provide#decorator * @description * - * Register a **service decorator** with the {@link AUTO.$injector $injector}. A service decorator - * intercepts the creation of a service, allowing it to override or modify the behaviour of the - * service. The object returned by the decorator may be the original service, or a new service - * object which replaces or wraps and delegates to the original service. + * Register a **decorator function** with the {@link auto.$injector $injector}. A decorator function + * intercepts the creation of a service, allowing it to override or modify the behavior of the + * service. The return value of the decorator function may be the original service, or a new service + * that replaces (or wraps and delegates to) the original service. + * + * You can find out more about using decorators in the {@link guide/decorators} guide. * * @param {string} name The name of the service to decorate. - * @param {function()} decorator This function will be invoked when the service needs to be - * instantiated and should return the decorated service instance. The function is called using - * the {@link AUTO.$injector#invoke injector.invoke} method and is therefore fully injectable. + * @param {Function|Array.} decorator This function will be invoked when the service needs to be + * provided and should return the decorated service instance. The function is called using + * the {@link auto.$injector#invoke injector.invoke} method and is therefore fully injectable. * Local injection arguments: * - * * `$delegate` - The original service instance, which can be monkey patched, configured, + * * `$delegate` - The original service instance, which can be replaced, monkey patched, configured, * decorated or delegated to. * * @example * Here we decorate the {@link ng.$log $log} service to convert warnings to errors by intercepting * calls to {@link ng.$log#error $log.warn()}. - *
    -   *   $provider.decorator('$log', ['$delegate', function($delegate) {
    +   * ```js
    +   *   $provide.decorator('$log', ['$delegate', function($delegate) {
      *     $delegate.warn = $delegate.error;
      *     return $delegate;
      *   }]);
    -   * 
    + * ``` */ - function createInjector(modulesToLoad) { + function createInjector(modulesToLoad, strictDi) { + strictDi = (strictDi === true); var INSTANTIATING = {}, - providerSuffix = 'Provider', - path = [], - loadedModules = new HashMap(), - providerCache = { - $provide: { - provider: supportObject(provider), - factory: supportObject(factory), - service: supportObject(service), - value: supportObject(value), - constant: supportObject(constant), - decorator: decorator + providerSuffix = 'Provider', + path = [], + loadedModules = new NgMap(), + providerCache = { + $provide: { + provider: supportObject(provider), + factory: supportObject(factory), + service: supportObject(service), + value: supportObject(value), + constant: supportObject(constant), + decorator: decorator + } + }, + providerInjector = (providerCache.$injector = + createInternalInjector(providerCache, function(serviceName, caller) { + if (angular.isString(caller)) { + path.push(caller); } - }, - providerInjector = (providerCache.$injector = - createInternalInjector(providerCache, function() { - throw $injectorMinErr('unpr', "Unknown provider: {0}", path.join(' <- ')); - })), - instanceCache = {}, - instanceInjector = (instanceCache.$injector = - createInternalInjector(instanceCache, function(servicename) { - var provider = providerInjector.get(servicename + providerSuffix); - return instanceInjector.invoke(provider.$get, provider); - })); + throw $injectorMinErr('unpr', 'Unknown provider: {0}', path.join(' <- ')); + })), + instanceCache = {}, + protoInstanceInjector = + createInternalInjector(instanceCache, function(serviceName, caller) { + var provider = providerInjector.get(serviceName + providerSuffix, caller); + return instanceInjector.invoke( + provider.$get, provider, undefined, serviceName); + }), + instanceInjector = protoInstanceInjector; - - forEach(loadModules(modulesToLoad), function(fn) { instanceInjector.invoke(fn || noop); }); + providerCache['$injector' + providerSuffix] = { $get: valueFn(protoInstanceInjector) }; + instanceInjector.modules = providerInjector.modules = createMap(); + var runBlocks = loadModules(modulesToLoad); + instanceInjector = protoInstanceInjector.get('$injector'); + instanceInjector.strictDi = strictDi; + forEach(runBlocks, function(fn) { if (fn) instanceInjector.invoke(fn); }); return instanceInjector; @@ -3571,12 +4863,26 @@ provider_ = providerInjector.instantiate(provider_); } if (!provider_.$get) { - throw $injectorMinErr('pget', "Provider '{0}' must define $get factory method.", name); + throw $injectorMinErr('pget', 'Provider \'{0}\' must define $get factory method.', name); } - return providerCache[name + providerSuffix] = provider_; + return (providerCache[name + providerSuffix] = provider_); } - function factory(name, factoryFn) { return provider(name, { $get: factoryFn }); } + function enforceReturnValue(name, factory) { + return /** @this */ function enforcedReturnValue() { + var result = instanceInjector.invoke(factory, this); + if (isUndefined(result)) { + throw $injectorMinErr('undef', 'Provider \'{0}\' must return a value from $get factory method.', name); + } + return result; + }; + } + + function factory(name, factoryFn, enforce) { + return provider(name, { + $get: enforce !== false ? enforceReturnValue(name, factoryFn) : factoryFn + }); + } function service(name, constructor) { return factory(name, ['$injector', function($injector) { @@ -3584,7 +4890,7 @@ }]); } - function value(name, val) { return factory(name, valueFn(val)); } + function value(name, val) { return factory(name, valueFn(val), false); } function constant(name, value) { assertNotHasOwnProperty(name, 'constant'); @@ -3594,7 +4900,7 @@ function decorator(serviceName, decorFn) { var origProvider = providerInjector.get(serviceName + providerSuffix), - orig$get = origProvider.$get; + orig$get = origProvider.$get; origProvider.$get = function() { var origInstance = instanceInjector.invoke(orig$get, origProvider); @@ -3605,23 +4911,30 @@ //////////////////////////////////// // Module Loading //////////////////////////////////// - function loadModules(modulesToLoad){ - var runBlocks = [], moduleFn, invokeQueue, i, ii; + function loadModules(modulesToLoad) { + assertArg(isUndefined(modulesToLoad) || isArray(modulesToLoad), 'modulesToLoad', 'not an array'); + var runBlocks = [], moduleFn; forEach(modulesToLoad, function(module) { if (loadedModules.get(module)) return; - loadedModules.put(module, true); + loadedModules.set(module, true); + + function runInvokeQueue(queue) { + var i, ii; + for (i = 0, ii = queue.length; i < ii; i++) { + var invokeArgs = queue[i], + provider = providerInjector.get(invokeArgs[0]); + + provider[invokeArgs[1]].apply(provider, invokeArgs[2]); + } + } try { if (isString(module)) { moduleFn = angularModule(module); + instanceInjector.modules[module] = moduleFn; runBlocks = runBlocks.concat(loadModules(moduleFn.requires)).concat(moduleFn._runBlocks); - - for(invokeQueue = moduleFn._invokeQueue, i = 0, ii = invokeQueue.length; i < ii; i++) { - var invokeArgs = invokeQueue[i], - provider = providerInjector.get(invokeArgs[0]); - - provider[invokeArgs[1]].apply(provider, invokeArgs[2]); - } + runInvokeQueue(moduleFn._invokeQueue); + runInvokeQueue(moduleFn._configBlocks); } else if (isFunction(module)) { runBlocks.push(providerInjector.invoke(module)); } else if (isArray(module)) { @@ -3633,16 +4946,16 @@ if (isArray(module)) { module = module[module.length - 1]; } - if (e.message && e.stack && e.stack.indexOf(e.message) == -1) { + if (e.message && e.stack && e.stack.indexOf(e.message) === -1) { // Safari & FF's stack traces don't contain error.message content // unlike those of Chrome and IE // So if stack doesn't contain message, we create a new string that contains both. // Since error.stack is read-only in Safari, I'm overriding e and not e.stack here. - /* jshint -W022 */ + // eslint-disable-next-line no-ex-assign e = e.message + '\n' + e.stack; } - throw $injectorMinErr('modulerr', "Failed to instantiate module {0} due to:\n{1}", - module, e.stack || e.message || e); + throw $injectorMinErr('modulerr', 'Failed to instantiate module {0} due to:\n{1}', + module, e.stack || e.message || e); } }); return runBlocks; @@ -3654,17 +4967,19 @@ function createInternalInjector(cache, factory) { - function getService(serviceName) { + function getService(serviceName, caller) { if (cache.hasOwnProperty(serviceName)) { if (cache[serviceName] === INSTANTIATING) { - throw $injectorMinErr('cdep', 'Circular dependency found: {0}', path.join(' <- ')); + throw $injectorMinErr('cdep', 'Circular dependency found: {0}', + serviceName + ' <- ' + path.join(' <- ')); } return cache[serviceName]; } else { try { path.unshift(serviceName); cache[serviceName] = INSTANTIATING; - return cache[serviceName] = factory(serviceName); + cache[serviceName] = factory(serviceName, caller); + return cache[serviceName]; } catch (err) { if (cache[serviceName] === INSTANTIATING) { delete cache[serviceName]; @@ -3676,52 +4991,76 @@ } } - function invoke(fn, self, locals){ - var args = [], - $inject = annotate(fn), - length, i, - key; - for(i = 0, length = $inject.length; i < length; i++) { - key = $inject[i]; + function injectionArgs(fn, locals, serviceName) { + var args = [], + $inject = createInjector.$$annotate(fn, strictDi, serviceName); + + for (var i = 0, length = $inject.length; i < length; i++) { + var key = $inject[i]; if (typeof key !== 'string') { throw $injectorMinErr('itkn', - 'Incorrect injection token! Expected service name as string, got {0}', key); + 'Incorrect injection token! Expected service name as string, got {0}', key); } - args.push( - locals && locals.hasOwnProperty(key) - ? locals[key] - : getService(key) - ); + args.push(locals && locals.hasOwnProperty(key) ? locals[key] : + getService(key, serviceName)); } - if (!fn.$inject) { - // this means that we must be an array. - fn = fn[length]; - } - - // http://jsperf.com/angularjs-invoke-apply-vs-switch - // #5388 - return fn.apply(self, args); + return args; } - function instantiate(Type, locals) { - var Constructor = function() {}, - instance, returnedValue; + function isClass(func) { + // Support: IE 9-11 only + // IE 9-11 do not support classes and IE9 leaks with the code below. + if (msie || typeof func !== 'function') { + return false; + } + var result = func.$$ngIsClass; + if (!isBoolean(result)) { + // Support: Edge 12-13 only + // See: https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/6156135/ + result = func.$$ngIsClass = /^(?:class\b|constructor\()/.test(stringifyFn(func)); + } + return result; + } + function invoke(fn, self, locals, serviceName) { + if (typeof locals === 'string') { + serviceName = locals; + locals = null; + } + + var args = injectionArgs(fn, locals, serviceName); + if (isArray(fn)) { + fn = fn[fn.length - 1]; + } + + if (!isClass(fn)) { + // http://jsperf.com/angularjs-invoke-apply-vs-switch + // #5388 + return fn.apply(self, args); + } else { + args.unshift(null); + return new (Function.prototype.bind.apply(fn, args))(); + } + } + + + function instantiate(Type, locals, serviceName) { // Check if Type is annotated and use just the given function at n-1 as parameter // e.g. someModule.factory('greeter', ['$window', function(renamed$window) {}]); - Constructor.prototype = (isArray(Type) ? Type[Type.length - 1] : Type).prototype; - instance = new Constructor(); - returnedValue = invoke(Type, instance, locals); - - return isObject(returnedValue) || isFunction(returnedValue) ? returnedValue : instance; + var ctor = (isArray(Type) ? Type[Type.length - 1] : Type); + var args = injectionArgs(Type, locals, serviceName); + // Empty object at position 0 is ignored for invocation with `new`, but required. + args.unshift(null); + return new (Function.prototype.bind.apply(ctor, args))(); } + return { invoke: invoke, instantiate: instantiate, get: getService, - annotate: annotate, + annotate: createInjector.$$annotate, has: function(name) { return providerCache.hasOwnProperty(name + providerSuffix) || cache.hasOwnProperty(name); } @@ -3729,100 +5068,275 @@ } } + createInjector.$$annotate = annotate; + /** - * @ngdoc function - * @name ng.$anchorScroll - * @requires $window - * @requires $location - * @requires $rootScope + * @ngdoc provider + * @name $anchorScrollProvider + * @this * * @description - * When called, it checks current value of `$location.hash()` and scroll to related element, - * according to rules specified in - * {@link http://dev.w3.org/html5/spec/Overview.html#the-indicated-part-of-the-document Html5 spec}. - * - * It also watches the `$location.hash()` and scrolls whenever it changes to match any anchor. - * This can be disabled by calling `$anchorScrollProvider.disableAutoScrolling()`. - * - * @example - - -
    - Go to bottom - You're at the bottom! -
    - - - function ScrollCtrl($scope, $location, $anchorScroll) { - $scope.gotoBottom = function (){ - // set the location.hash to the id of - // the element you wish to scroll to. - $location.hash('bottom'); - - // call $anchorScroll() - $anchorScroll(); - } - } - - - #scrollArea { - height: 350px; - overflow: auto; - } - - #bottom { - display: block; - margin-top: 2000px; - } - - + * Use `$anchorScrollProvider` to disable automatic scrolling whenever + * {@link ng.$location#hash $location.hash()} changes. */ function $AnchorScrollProvider() { var autoScrollingEnabled = true; + /** + * @ngdoc method + * @name $anchorScrollProvider#disableAutoScrolling + * + * @description + * By default, {@link ng.$anchorScroll $anchorScroll()} will automatically detect changes to + * {@link ng.$location#hash $location.hash()} and scroll to the element matching the new hash.
    + * Use this method to disable automatic scrolling. + * + * If automatic scrolling is disabled, one must explicitly call + * {@link ng.$anchorScroll $anchorScroll()} in order to scroll to the element related to the + * current hash. + */ this.disableAutoScrolling = function() { autoScrollingEnabled = false; }; + /** + * @ngdoc service + * @name $anchorScroll + * @kind function + * @requires $window + * @requires $location + * @requires $rootScope + * + * @description + * When called, it scrolls to the element related to the specified `hash` or (if omitted) to the + * current value of {@link ng.$location#hash $location.hash()}, according to the rules specified + * in the + * [HTML5 spec](http://www.w3.org/html/wg/drafts/html/master/browsers.html#an-indicated-part-of-the-document). + * + * It also watches the {@link ng.$location#hash $location.hash()} and automatically scrolls to + * match any anchor whenever it changes. This can be disabled by calling + * {@link ng.$anchorScrollProvider#disableAutoScrolling $anchorScrollProvider.disableAutoScrolling()}. + * + * Additionally, you can use its {@link ng.$anchorScroll#yOffset yOffset} property to specify a + * vertical scroll-offset (either fixed or dynamic). + * + * @param {string=} hash The hash specifying the element to scroll to. If omitted, the value of + * {@link ng.$location#hash $location.hash()} will be used. + * + * @property {(number|function|jqLite)} yOffset + * If set, specifies a vertical scroll-offset. This is often useful when there are fixed + * positioned elements at the top of the page, such as navbars, headers etc. + * + * `yOffset` can be specified in various ways: + * - **number**: A fixed number of pixels to be used as offset.

    + * - **function**: A getter function called everytime `$anchorScroll()` is executed. Must return + * a number representing the offset (in pixels).

    + * - **jqLite**: A jqLite/jQuery element to be used for specifying the offset. The distance from + * the top of the page to the element's bottom will be used as offset.
    + * **Note**: The element will be taken into account only as long as its `position` is set to + * `fixed`. This option is useful, when dealing with responsive navbars/headers that adjust + * their height and/or positioning according to the viewport's size. + * + *
    + *
    + * In order for `yOffset` to work properly, scrolling should take place on the document's root and + * not some child element. + *
    + * + * @example + + +
    + Go to bottom + You're at the bottom! +
    +
    + + angular.module('anchorScrollExample', []) + .controller('ScrollController', ['$scope', '$location', '$anchorScroll', + function($scope, $location, $anchorScroll) { + $scope.gotoBottom = function() { + // set the location.hash to the id of + // the element you wish to scroll to. + $location.hash('bottom'); + + // call $anchorScroll() + $anchorScroll(); + }; + }]); + + + #scrollArea { + height: 280px; + overflow: auto; + } + + #bottom { + display: block; + margin-top: 2000px; + } + +
    + * + *
    + * The example below illustrates the use of a vertical scroll-offset (specified as a fixed value). + * See {@link ng.$anchorScroll#yOffset $anchorScroll.yOffset} for more details. + * + * @example + + + +
    + Anchor {{x}} of 5 +
    +
    + + angular.module('anchorScrollOffsetExample', []) + .run(['$anchorScroll', function($anchorScroll) { + $anchorScroll.yOffset = 50; // always scroll by 50 extra pixels + }]) + .controller('headerCtrl', ['$anchorScroll', '$location', '$scope', + function($anchorScroll, $location, $scope) { + $scope.gotoAnchor = function(x) { + var newHash = 'anchor' + x; + if ($location.hash() !== newHash) { + // set the $location.hash to `newHash` and + // $anchorScroll will automatically scroll to it + $location.hash('anchor' + x); + } else { + // call $anchorScroll() explicitly, + // since $location.hash hasn't changed + $anchorScroll(); + } + }; + } + ]); + + + body { + padding-top: 50px; + } + + .anchor { + border: 2px dashed DarkOrchid; + padding: 10px 10px 200px 10px; + } + + .fixed-header { + background-color: rgba(0, 0, 0, 0.2); + height: 50px; + position: fixed; + top: 0; left: 0; right: 0; + } + + .fixed-header > a { + display: inline-block; + margin: 5px 15px; + } + +
    + */ this.$get = ['$window', '$location', '$rootScope', function($window, $location, $rootScope) { var document = $window.document; - // helper function to get first anchor from a NodeList - // can't use filter.filter, as it accepts only instances of Array - // and IE can't convert NodeList to an array using [].slice - // TODO(vojta): use filter if we change it to accept lists as well + // Helper function to get first anchor from a NodeList + // (using `Array#some()` instead of `angular#forEach()` since it's more performant + // and working in all supported browsers.) function getFirstAnchor(list) { var result = null; - forEach(list, function(element) { - if (!result && lowercase(element.nodeName) === 'a') result = element; + Array.prototype.some.call(list, function(element) { + if (nodeName_(element) === 'a') { + result = element; + return true; + } }); return result; } - function scroll() { - var hash = $location.hash(), elm; + function getYOffset() { + + var offset = scroll.yOffset; + + if (isFunction(offset)) { + offset = offset(); + } else if (isElement(offset)) { + var elem = offset[0]; + var style = $window.getComputedStyle(elem); + if (style.position !== 'fixed') { + offset = 0; + } else { + offset = elem.getBoundingClientRect().bottom; + } + } else if (!isNumber(offset)) { + offset = 0; + } + + return offset; + } + + function scrollTo(elem) { + if (elem) { + elem.scrollIntoView(); + + var offset = getYOffset(); + + if (offset) { + // `offset` is the number of pixels we should scroll UP in order to align `elem` properly. + // This is true ONLY if the call to `elem.scrollIntoView()` initially aligns `elem` at the + // top of the viewport. + // + // IF the number of pixels from the top of `elem` to the end of the page's content is less + // than the height of the viewport, then `elem.scrollIntoView()` will align the `elem` some + // way down the page. + // + // This is often the case for elements near the bottom of the page. + // + // In such cases we do not need to scroll the whole `offset` up, just the difference between + // the top of the element and the offset, which is enough to align the top of `elem` at the + // desired position. + var elemTop = elem.getBoundingClientRect().top; + $window.scrollBy(0, elemTop - offset); + } + } else { + $window.scrollTo(0, 0); + } + } + + function scroll(hash) { + // Allow numeric hashes + hash = isString(hash) ? hash : isNumber(hash) ? hash.toString() : $location.hash(); + var elm; // empty hash, scroll to the top of the page - if (!hash) $window.scrollTo(0, 0); + if (!hash) scrollTo(null); // element with given id - else if ((elm = document.getElementById(hash))) elm.scrollIntoView(); + else if ((elm = document.getElementById(hash))) scrollTo(elm); // first anchor with given name :-D - else if ((elm = getFirstAnchor(document.getElementsByName(hash)))) elm.scrollIntoView(); + else if ((elm = getFirstAnchor(document.getElementsByName(hash)))) scrollTo(elm); - // no element and hash == 'top', scroll to the top of the page - else if (hash === 'top') $window.scrollTo(0, 0); + // no element and hash === 'top', scroll to the top of the page + else if (hash === 'top') scrollTo(null); } // does not scroll when user clicks on anchor link that is currently on // (no url change, no $location.hash() change), browser native does scroll if (autoScrollingEnabled) { $rootScope.$watch(function autoScrollWatch() {return $location.hash();}, - function autoScrollWatchAction() { + function autoScrollWatchAction(newVal, oldVal) { + // skip the initial scroll if $location.hash is empty + if (newVal === oldVal && newVal === '') return; + + jqLiteDocumentLoaded(function() { $rootScope.$evalAsync(scroll); }); + }); } return scroll; @@ -3830,236 +5344,981 @@ } var $animateMinErr = minErr('$animate'); + var ELEMENT_NODE = 1; + var NG_ANIMATE_CLASSNAME = 'ng-animate'; + + function mergeClasses(a,b) { + if (!a && !b) return ''; + if (!a) return b; + if (!b) return a; + if (isArray(a)) a = a.join(' '); + if (isArray(b)) b = b.join(' '); + return a + ' ' + b; + } + + function extractElementNode(element) { + for (var i = 0; i < element.length; i++) { + var elm = element[i]; + if (elm.nodeType === ELEMENT_NODE) { + return elm; + } + } + } + + function splitClasses(classes) { + if (isString(classes)) { + classes = classes.split(' '); + } + + // Use createMap() to prevent class assumptions involving property names in + // Object.prototype + var obj = createMap(); + forEach(classes, function(klass) { + // sometimes the split leaves empty string values + // incase extra spaces were applied to the options + if (klass.length) { + obj[klass] = true; + } + }); + return obj; + } + +// if any other type of options value besides an Object value is +// passed into the $animate.method() animation then this helper code +// will be run which will ignore it. While this patch is not the +// greatest solution to this, a lot of existing plugins depend on +// $animate to either call the callback (< 1.2) or return a promise +// that can be changed. This helper function ensures that the options +// are wiped clean incase a callback function is provided. + function prepareAnimateOptions(options) { + return isObject(options) + ? options + : {}; + } + + var $$CoreAnimateJsProvider = /** @this */ function() { + this.$get = noop; + }; + +// this is prefixed with Core since it conflicts with +// the animateQueueProvider defined in ngAnimate/animateQueue.js + var $$CoreAnimateQueueProvider = /** @this */ function() { + var postDigestQueue = new NgMap(); + var postDigestElements = []; + + this.$get = ['$$AnimateRunner', '$rootScope', + function($$AnimateRunner, $rootScope) { + return { + enabled: noop, + on: noop, + off: noop, + pin: noop, + + push: function(element, event, options, domOperation) { + if (domOperation) { + domOperation(); + } + + options = options || {}; + if (options.from) { + element.css(options.from); + } + if (options.to) { + element.css(options.to); + } + + if (options.addClass || options.removeClass) { + addRemoveClassesPostDigest(element, options.addClass, options.removeClass); + } + + var runner = new $$AnimateRunner(); + + // since there are no animations to run the runner needs to be + // notified that the animation call is complete. + runner.complete(); + return runner; + } + }; + + + function updateData(data, classes, value) { + var changed = false; + if (classes) { + classes = isString(classes) ? classes.split(' ') : + isArray(classes) ? classes : []; + forEach(classes, function(className) { + if (className) { + changed = true; + data[className] = value; + } + }); + } + return changed; + } + + function handleCSSClassChanges() { + forEach(postDigestElements, function(element) { + var data = postDigestQueue.get(element); + if (data) { + var existing = splitClasses(element.attr('class')); + var toAdd = ''; + var toRemove = ''; + forEach(data, function(status, className) { + var hasClass = !!existing[className]; + if (status !== hasClass) { + if (status) { + toAdd += (toAdd.length ? ' ' : '') + className; + } else { + toRemove += (toRemove.length ? ' ' : '') + className; + } + } + }); + + forEach(element, function(elm) { + if (toAdd) { + jqLiteAddClass(elm, toAdd); + } + if (toRemove) { + jqLiteRemoveClass(elm, toRemove); + } + }); + postDigestQueue.delete(element); + } + }); + postDigestElements.length = 0; + } + + + function addRemoveClassesPostDigest(element, add, remove) { + var data = postDigestQueue.get(element) || {}; + + var classesAdded = updateData(data, add, true); + var classesRemoved = updateData(data, remove, false); + + if (classesAdded || classesRemoved) { + + postDigestQueue.set(element, data); + postDigestElements.push(element); + + if (postDigestElements.length === 1) { + $rootScope.$$postDigest(handleCSSClassChanges); + } + } + } + }]; + }; /** - * @ngdoc object - * @name ng.$animateProvider + * @ngdoc provider + * @name $animateProvider * * @description * Default implementation of $animate that doesn't perform any animations, instead just - * synchronously performs DOM - * updates and calls done() callbacks. + * synchronously performs DOM updates and resolves the returned runner promise. * - * In order to enable animations the ngAnimate module has to be loaded. + * In order to enable animations the `ngAnimate` module has to be loaded. * - * To see the functional implementation check out src/ngAnimate/animate.js + * To see the functional implementation check out `src/ngAnimate/animate.js`. */ - var $AnimateProvider = ['$provide', function($provide) { - - - this.$$selectors = {}; + var $AnimateProvider = ['$provide', /** @this */ function($provide) { + var provider = this; + var classNameFilter = null; + var customFilter = null; + this.$$registeredAnimations = Object.create(null); /** - * @ngdoc function - * @name ng.$animateProvider#register - * @methodOf ng.$animateProvider + * @ngdoc method + * @name $animateProvider#register * * @description * Registers a new injectable animation factory function. The factory function produces the * animation object which contains callback functions for each event that is expected to be * animated. * - * * `eventFn`: `function(Element, doneFunction)` The element to animate, the `doneFunction` - * must be called once the element animation is complete. If a function is returned then the - * animation service will use this function to cancel the animation whenever a cancel event is - * triggered. + * * `eventFn`: `function(element, ... , doneFunction, options)` + * The element to animate, the `doneFunction` and the options fed into the animation. Depending + * on the type of animation additional arguments will be injected into the animation function. The + * list below explains the function signatures for the different animation methods: * + * - setClass: function(element, addedClasses, removedClasses, doneFunction, options) + * - addClass: function(element, addedClasses, doneFunction, options) + * - removeClass: function(element, removedClasses, doneFunction, options) + * - enter, leave, move: function(element, doneFunction, options) + * - animate: function(element, fromStyles, toStyles, doneFunction, options) * - *
    +     *   Make sure to trigger the `doneFunction` once the animation is fully complete.
    +     *
    +     * ```js
          *   return {
    -     *     eventFn : function(element, done) {
    -     *       //code to run the animation
    -     *       //once complete, then run done()
    -     *       return function cancellationFunction() {
    -     *         //code to cancel the animation
    -     *       }
    -     *     }
    -     *   }
    -     *
    + * //enter, leave, move signature + * eventFn : function(element, done, options) { + * //code to run the animation + * //once complete, then run done() + * return function endFunction(wasCancelled) { + * //code to cancel the animation + * } + * } + * } + * ``` * - * @param {string} name The name of the animation. - * @param {function} factory The factory function that will be executed to return the animation + * @param {string} name The name of the animation (this is what the class-based CSS value will be compared to). + * @param {Function} factory The factory function that will be executed to return the animation * object. */ this.register = function(name, factory) { + if (name && name.charAt(0) !== '.') { + throw $animateMinErr('notcsel', 'Expecting class selector starting with \'.\' got \'{0}\'.', name); + } + var key = name + '-animation'; - if (name && name.charAt(0) != '.') throw $animateMinErr('notcsel', - "Expecting class selector starting with '.' got '{0}'.", name); - this.$$selectors[name.substr(1)] = key; + provider.$$registeredAnimations[name.substr(1)] = key; $provide.factory(key, factory); }; /** - * @ngdoc function - * @name ng.$animateProvider#classNameFilter - * @methodOf ng.$animateProvider + * @ngdoc method + * @name $animateProvider#customFilter + * + * @description + * Sets and/or returns the custom filter function that is used to "filter" animations, i.e. + * determine if an animation is allowed or not. When no filter is specified (the default), no + * animation will be blocked. Setting the `customFilter` value will only allow animations for + * which the filter function's return value is truthy. + * + * This allows to easily create arbitrarily complex rules for filtering animations, such as + * allowing specific events only, or enabling animations on specific subtrees of the DOM, etc. + * Filtering animations can also boost performance for low-powered devices, as well as + * applications containing a lot of structural operations. + * + *
    + * **Best Practice:** + * Keep the filtering function as lean as possible, because it will be called for each DOM + * action (e.g. insertion, removal, class change) performed by "animation-aware" directives. + * See {@link guide/animations#which-directives-support-animations- here} for a list of built-in + * directives that support animations. + * Performing computationally expensive or time-consuming operations on each call of the + * filtering function can make your animations sluggish. + *
    + * + * **Note:** If present, `customFilter` will be checked before + * {@link $animateProvider#classNameFilter classNameFilter}. + * + * @param {Function=} filterFn - The filter function which will be used to filter all animations. + * If a falsy value is returned, no animation will be performed. The function will be called + * with the following arguments: + * - **node** `{DOMElement}` - The DOM element to be animated. + * - **event** `{String}` - The name of the animation event (e.g. `enter`, `leave`, `addClass` + * etc). + * - **options** `{Object}` - A collection of options/styles used for the animation. + * @return {Function} The current filter function or `null` if there is none set. + */ + this.customFilter = function(filterFn) { + if (arguments.length === 1) { + customFilter = isFunction(filterFn) ? filterFn : null; + } + + return customFilter; + }; + + /** + * @ngdoc method + * @name $animateProvider#classNameFilter * * @description * Sets and/or returns the CSS class regular expression that is checked when performing * an animation. Upon bootstrap the classNameFilter value is not set at all and will - * therefore enable $animate to attempt to perform an animation on any element. - * When setting the classNameFilter value, animations will only be performed on elements + * therefore enable $animate to attempt to perform an animation on any element that is triggered. + * When setting the `classNameFilter` value, animations will only be performed on elements * that successfully match the filter expression. This in turn can boost performance * for low-powered devices as well as applications containing a lot of structural operations. + * + * **Note:** If present, `classNameFilter` will be checked after + * {@link $animateProvider#customFilter customFilter}. If `customFilter` is present and returns + * false, `classNameFilter` will not be checked. + * * @param {RegExp=} expression The className expression which will be checked against all animations * @return {RegExp} The current CSS className expression value. If null then there is no expression value */ this.classNameFilter = function(expression) { - if(arguments.length === 1) { - this.$$classNameFilter = (expression instanceof RegExp) ? expression : null; + if (arguments.length === 1) { + classNameFilter = (expression instanceof RegExp) ? expression : null; + if (classNameFilter) { + var reservedRegex = new RegExp('[(\\s|\\/)]' + NG_ANIMATE_CLASSNAME + '[(\\s|\\/)]'); + if (reservedRegex.test(classNameFilter.toString())) { + classNameFilter = null; + throw $animateMinErr('nongcls', '$animateProvider.classNameFilter(regex) prohibits accepting a regex value which matches/contains the "{0}" CSS class.', NG_ANIMATE_CLASSNAME); + } + } } - return this.$$classNameFilter; + return classNameFilter; }; - this.$get = ['$timeout', function($timeout) { + this.$get = ['$$animateQueue', function($$animateQueue) { + function domInsert(element, parentElement, afterElement) { + // if for some reason the previous element was removed + // from the dom sometime before this code runs then let's + // just stick to using the parent element as the anchor + if (afterElement) { + var afterNode = extractElementNode(afterElement); + if (afterNode && !afterNode.parentNode && !afterNode.previousElementSibling) { + afterElement = null; + } + } + if (afterElement) { + afterElement.after(element); + } else { + parentElement.prepend(element); + } + } /** + * @ngdoc service + * @name $animate + * @description The $animate service exposes a series of DOM utility methods that provide support + * for animation hooks. The default behavior is the application of DOM operations, however, + * when an animation is detected (and animations are enabled), $animate will do the heavy lifting + * to ensure that animation runs with the triggered DOM operation. * - * @ngdoc object - * @name ng.$animate - * @description The $animate service provides rudimentary DOM manipulation functions to - * insert, remove and move elements within the DOM, as well as adding and removing classes. - * This service is the core service used by the ngAnimate $animator service which provides - * high-level animation hooks for CSS and JavaScript. + * By default $animate doesn't trigger any animations. This is because the `ngAnimate` module isn't + * included and only when it is active then the animation hooks that `$animate` triggers will be + * functional. Once active then all structural `ng-` directives will trigger animations as they perform + * their DOM-related operations (enter, leave and move). Other directives such as `ngClass`, + * `ngShow`, `ngHide` and `ngMessages` also provide support for animations. * - * $animate is available in the AngularJS core, however, the ngAnimate module must be included - * to enable full out animation support. Otherwise, $animate will only perform simple DOM - * manipulation operations. + * It is recommended that the`$animate` service is always used when executing DOM-related procedures within directives. * - * To learn more about enabling animation support, click here to visit the {@link ngAnimate - * ngAnimate module page} as well as the {@link ngAnimate.$animate ngAnimate $animate service - * page}. + * To learn more about enabling animation support, click here to visit the + * {@link ngAnimate ngAnimate module page}. */ return { + // we don't call it directly since non-existant arguments may + // be interpreted as null within the sub enabled function /** * - * @ngdoc function - * @name ng.$animate#enter - * @methodOf ng.$animate - * @function - * @description Inserts the element into the DOM either after the `after` element or within - * the `parent` element. Once complete, the done() callback will be fired (if provided). - * @param {jQuery/jqLite element} element the element which will be inserted into the DOM - * @param {jQuery/jqLite element} parent the parent element which will append the element as - * a child (if the after element is not present) - * @param {jQuery/jqLite element} after the sibling element which will append the element - * after itself - * @param {function=} done callback function that will be called after the element has been - * inserted into the DOM + * @ngdoc method + * @name $animate#on + * @kind function + * @description Sets up an event listener to fire whenever the animation event (enter, leave, move, etc...) + * has fired on the given element or among any of its children. Once the listener is fired, the provided callback + * is fired with the following params: + * + * ```js + * $animate.on('enter', container, + * function callback(element, phase) { + * // cool we detected an enter animation within the container + * } + * ); + * ``` + * + * @param {string} event the animation event that will be captured (e.g. enter, leave, move, addClass, removeClass, etc...) + * @param {DOMElement} container the container element that will capture each of the animation events that are fired on itself + * as well as among its children + * @param {Function} callback the callback function that will be fired when the listener is triggered + * + * The arguments present in the callback function are: + * * `element` - The captured DOM element that the animation was fired on. + * * `phase` - The phase of the animation. The two possible phases are **start** (when the animation starts) and **close** (when it ends). */ - enter : function(element, parent, after, done) { - if (after) { - after.after(element); - } else { - if (!parent || !parent[0]) { - parent = after.parent(); - } - parent.append(element); + on: $$animateQueue.on, + + /** + * + * @ngdoc method + * @name $animate#off + * @kind function + * @description Deregisters an event listener based on the event which has been associated with the provided element. This method + * can be used in three different ways depending on the arguments: + * + * ```js + * // remove all the animation event listeners listening for `enter` + * $animate.off('enter'); + * + * // remove listeners for all animation events from the container element + * $animate.off(container); + * + * // remove all the animation event listeners listening for `enter` on the given element and its children + * $animate.off('enter', container); + * + * // remove the event listener function provided by `callback` that is set + * // to listen for `enter` on the given `container` as well as its children + * $animate.off('enter', container, callback); + * ``` + * + * @param {string|DOMElement} event|container the animation event (e.g. enter, leave, move, + * addClass, removeClass, etc...), or the container element. If it is the element, all other + * arguments are ignored. + * @param {DOMElement=} container the container element the event listener was placed on + * @param {Function=} callback the callback function that was registered as the listener + */ + off: $$animateQueue.off, + + /** + * @ngdoc method + * @name $animate#pin + * @kind function + * @description Associates the provided element with a host parent element to allow the element to be animated even if it exists + * outside of the DOM structure of the Angular application. By doing so, any animation triggered via `$animate` can be issued on the + * element despite being outside the realm of the application or within another application. Say for example if the application + * was bootstrapped on an element that is somewhere inside of the `` tag, but we wanted to allow for an element to be situated + * as a direct child of `document.body`, then this can be achieved by pinning the element via `$animate.pin(element)`. Keep in mind + * that calling `$animate.pin(element, parentElement)` will not actually insert into the DOM anywhere; it will just create the association. + * + * Note that this feature is only active when the `ngAnimate` module is used. + * + * @param {DOMElement} element the external element that will be pinned + * @param {DOMElement} parentElement the host parent element that will be associated with the external element + */ + pin: $$animateQueue.pin, + + /** + * + * @ngdoc method + * @name $animate#enabled + * @kind function + * @description Used to get and set whether animations are enabled or not on the entire application or on an element and its children. This + * function can be called in four ways: + * + * ```js + * // returns true or false + * $animate.enabled(); + * + * // changes the enabled state for all animations + * $animate.enabled(false); + * $animate.enabled(true); + * + * // returns true or false if animations are enabled for an element + * $animate.enabled(element); + * + * // changes the enabled state for an element and its children + * $animate.enabled(element, true); + * $animate.enabled(element, false); + * ``` + * + * @param {DOMElement=} element the element that will be considered for checking/setting the enabled state + * @param {boolean=} enabled whether or not the animations will be enabled for the element + * + * @return {boolean} whether or not animations are enabled + */ + enabled: $$animateQueue.enabled, + + /** + * @ngdoc method + * @name $animate#cancel + * @kind function + * @description Cancels the provided animation. + * + * @param {Promise} animationPromise The animation promise that is returned when an animation is started. + */ + cancel: function(runner) { + if (runner.end) { + runner.end(); } - done && $timeout(done, 0, false); }, /** * - * @ngdoc function - * @name ng.$animate#leave - * @methodOf ng.$animate - * @function - * @description Removes the element from the DOM. Once complete, the done() callback will be - * fired (if provided). - * @param {jQuery/jqLite element} element the element which will be removed from the DOM - * @param {function=} done callback function that will be called after the element has been - * removed from the DOM + * @ngdoc method + * @name $animate#enter + * @kind function + * @description Inserts the element into the DOM either after the `after` element (if provided) or + * as the first child within the `parent` element and then triggers an animation. + * A promise is returned that will be resolved during the next digest once the animation + * has completed. + * + * @param {DOMElement} element the element which will be inserted into the DOM + * @param {DOMElement} parent the parent element which will append the element as + * a child (so long as the after element is not present) + * @param {DOMElement=} after the sibling element after which the element will be appended + * @param {object=} options an optional collection of options/styles that will be applied to the element. + * The object can have the following properties: + * + * - **addClass** - `{string}` - space-separated CSS classes to add to element + * - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to` + * - **removeClass** - `{string}` - space-separated CSS classes to remove from element + * - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from` + * + * @return {Promise} the animation callback promise */ - leave : function(element, done) { - element.remove(); - done && $timeout(done, 0, false); + enter: function(element, parent, after, options) { + parent = parent && jqLite(parent); + after = after && jqLite(after); + parent = parent || after.parent(); + domInsert(element, parent, after); + return $$animateQueue.push(element, 'enter', prepareAnimateOptions(options)); }, /** * - * @ngdoc function - * @name ng.$animate#move - * @methodOf ng.$animate - * @function - * @description Moves the position of the provided element within the DOM to be placed - * either after the `after` element or inside of the `parent` element. Once complete, the - * done() callback will be fired (if provided). + * @ngdoc method + * @name $animate#move + * @kind function + * @description Inserts (moves) the element into its new position in the DOM either after + * the `after` element (if provided) or as the first child within the `parent` element + * and then triggers an animation. A promise is returned that will be resolved + * during the next digest once the animation has completed. * - * @param {jQuery/jqLite element} element the element which will be moved around within the - * DOM - * @param {jQuery/jqLite element} parent the parent element where the element will be - * inserted into (if the after element is not present) - * @param {jQuery/jqLite element} after the sibling element where the element will be - * positioned next to - * @param {function=} done the callback function (if provided) that will be fired after the - * element has been moved to its new position + * @param {DOMElement} element the element which will be moved into the new DOM position + * @param {DOMElement} parent the parent element which will append the element as + * a child (so long as the after element is not present) + * @param {DOMElement=} after the sibling element after which the element will be appended + * @param {object=} options an optional collection of options/styles that will be applied to the element. + * The object can have the following properties: + * + * - **addClass** - `{string}` - space-separated CSS classes to add to element + * - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to` + * - **removeClass** - `{string}` - space-separated CSS classes to remove from element + * - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from` + * + * @return {Promise} the animation callback promise */ - move : function(element, parent, after, done) { - // Do not remove element before insert. Removing will cause data associated with the - // element to be dropped. Insert will implicitly do the remove. - this.enter(element, parent, after, done); + move: function(element, parent, after, options) { + parent = parent && jqLite(parent); + after = after && jqLite(after); + parent = parent || after.parent(); + domInsert(element, parent, after); + return $$animateQueue.push(element, 'move', prepareAnimateOptions(options)); }, /** + * @ngdoc method + * @name $animate#leave + * @kind function + * @description Triggers an animation and then removes the element from the DOM. + * When the function is called a promise is returned that will be resolved during the next + * digest once the animation has completed. * - * @ngdoc function - * @name ng.$animate#addClass - * @methodOf ng.$animate - * @function - * @description Adds the provided className CSS class value to the provided element. Once - * complete, the done() callback will be fired (if provided). - * @param {jQuery/jqLite element} element the element which will have the className value - * added to it - * @param {string} className the CSS class which will be added to the element - * @param {function=} done the callback function (if provided) that will be fired after the - * className value has been added to the element + * @param {DOMElement} element the element which will be removed from the DOM + * @param {object=} options an optional collection of options/styles that will be applied to the element. + * The object can have the following properties: + * + * - **addClass** - `{string}` - space-separated CSS classes to add to element + * - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to` + * - **removeClass** - `{string}` - space-separated CSS classes to remove from element + * - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from` + * + * @return {Promise} the animation callback promise */ - addClass : function(element, className, done) { - className = isString(className) ? - className : - isArray(className) ? className.join(' ') : ''; - forEach(element, function (element) { - jqLiteAddClass(element, className); + leave: function(element, options) { + return $$animateQueue.push(element, 'leave', prepareAnimateOptions(options), function() { + element.remove(); }); - done && $timeout(done, 0, false); }, /** + * @ngdoc method + * @name $animate#addClass + * @kind function * - * @ngdoc function - * @name ng.$animate#removeClass - * @methodOf ng.$animate - * @function - * @description Removes the provided className CSS class value from the provided element. - * Once complete, the done() callback will be fired (if provided). - * @param {jQuery/jqLite element} element the element which will have the className value - * removed from it - * @param {string} className the CSS class which will be removed from the element - * @param {function=} done the callback function (if provided) that will be fired after the - * className value has been removed from the element + * @description Triggers an addClass animation surrounding the addition of the provided CSS class(es). Upon + * execution, the addClass operation will only be handled after the next digest and it will not trigger an + * animation if element already contains the CSS class or if the class is removed at a later step. + * Note that class-based animations are treated differently compared to structural animations + * (like enter, move and leave) since the CSS classes may be added/removed at different points + * depending if CSS or JavaScript animations are used. + * + * @param {DOMElement} element the element which the CSS classes will be applied to + * @param {string} className the CSS class(es) that will be added (multiple classes are separated via spaces) + * @param {object=} options an optional collection of options/styles that will be applied to the element. + * The object can have the following properties: + * + * - **addClass** - `{string}` - space-separated CSS classes to add to element + * - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to` + * - **removeClass** - `{string}` - space-separated CSS classes to remove from element + * - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from` + * + * @return {Promise} the animation callback promise */ - removeClass : function(element, className, done) { - className = isString(className) ? - className : - isArray(className) ? className.join(' ') : ''; - forEach(element, function (element) { - jqLiteRemoveClass(element, className); - }); - done && $timeout(done, 0, false); + addClass: function(element, className, options) { + options = prepareAnimateOptions(options); + options.addClass = mergeClasses(options.addclass, className); + return $$animateQueue.push(element, 'addClass', options); }, - enabled : noop + /** + * @ngdoc method + * @name $animate#removeClass + * @kind function + * + * @description Triggers a removeClass animation surrounding the removal of the provided CSS class(es). Upon + * execution, the removeClass operation will only be handled after the next digest and it will not trigger an + * animation if element does not contain the CSS class or if the class is added at a later step. + * Note that class-based animations are treated differently compared to structural animations + * (like enter, move and leave) since the CSS classes may be added/removed at different points + * depending if CSS or JavaScript animations are used. + * + * @param {DOMElement} element the element which the CSS classes will be applied to + * @param {string} className the CSS class(es) that will be removed (multiple classes are separated via spaces) + * @param {object=} options an optional collection of options/styles that will be applied to the element. + * The object can have the following properties: + * + * - **addClass** - `{string}` - space-separated CSS classes to add to element + * - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to` + * - **removeClass** - `{string}` - space-separated CSS classes to remove from element + * - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from` + * + * @return {Promise} the animation callback promise + */ + removeClass: function(element, className, options) { + options = prepareAnimateOptions(options); + options.removeClass = mergeClasses(options.removeClass, className); + return $$animateQueue.push(element, 'removeClass', options); + }, + + /** + * @ngdoc method + * @name $animate#setClass + * @kind function + * + * @description Performs both the addition and removal of a CSS classes on an element and (during the process) + * triggers an animation surrounding the class addition/removal. Much like `$animate.addClass` and + * `$animate.removeClass`, `setClass` will only evaluate the classes being added/removed once a digest has + * passed. Note that class-based animations are treated differently compared to structural animations + * (like enter, move and leave) since the CSS classes may be added/removed at different points + * depending if CSS or JavaScript animations are used. + * + * @param {DOMElement} element the element which the CSS classes will be applied to + * @param {string} add the CSS class(es) that will be added (multiple classes are separated via spaces) + * @param {string} remove the CSS class(es) that will be removed (multiple classes are separated via spaces) + * @param {object=} options an optional collection of options/styles that will be applied to the element. + * The object can have the following properties: + * + * - **addClass** - `{string}` - space-separated CSS classes to add to element + * - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to` + * - **removeClass** - `{string}` - space-separated CSS classes to remove from element + * - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from` + * + * @return {Promise} the animation callback promise + */ + setClass: function(element, add, remove, options) { + options = prepareAnimateOptions(options); + options.addClass = mergeClasses(options.addClass, add); + options.removeClass = mergeClasses(options.removeClass, remove); + return $$animateQueue.push(element, 'setClass', options); + }, + + /** + * @ngdoc method + * @name $animate#animate + * @kind function + * + * @description Performs an inline animation on the element which applies the provided to and from CSS styles to the element. + * If any detected CSS transition, keyframe or JavaScript matches the provided className value, then the animation will take + * on the provided styles. For example, if a transition animation is set for the given className, then the provided `from` and + * `to` styles will be applied alongside the given transition. If the CSS style provided in `from` does not have a corresponding + * style in `to`, the style in `from` is applied immediately, and no animation is run. + * If a JavaScript animation is detected then the provided styles will be given in as function parameters into the `animate` + * method (or as part of the `options` parameter): + * + * ```js + * ngModule.animation('.my-inline-animation', function() { + * return { + * animate : function(element, from, to, done, options) { + * //animation + * done(); + * } + * } + * }); + * ``` + * + * @param {DOMElement} element the element which the CSS styles will be applied to + * @param {object} from the from (starting) CSS styles that will be applied to the element and across the animation. + * @param {object} to the to (destination) CSS styles that will be applied to the element and across the animation. + * @param {string=} className an optional CSS class that will be applied to the element for the duration of the animation. If + * this value is left as empty then a CSS class of `ng-inline-animate` will be applied to the element. + * (Note that if no animation is detected then this value will not be applied to the element.) + * @param {object=} options an optional collection of options/styles that will be applied to the element. + * The object can have the following properties: + * + * - **addClass** - `{string}` - space-separated CSS classes to add to element + * - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to` + * - **removeClass** - `{string}` - space-separated CSS classes to remove from element + * - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from` + * + * @return {Promise} the animation callback promise + */ + animate: function(element, from, to, className, options) { + options = prepareAnimateOptions(options); + options.from = options.from ? extend(options.from, from) : from; + options.to = options.to ? extend(options.to, to) : to; + + className = className || 'ng-inline-animate'; + options.tempClasses = mergeClasses(options.tempClasses, className); + return $$animateQueue.push(element, 'animate', options); + } }; }]; }]; + var $$AnimateAsyncRunFactoryProvider = /** @this */ function() { + this.$get = ['$$rAF', function($$rAF) { + var waitQueue = []; + + function waitForTick(fn) { + waitQueue.push(fn); + if (waitQueue.length > 1) return; + $$rAF(function() { + for (var i = 0; i < waitQueue.length; i++) { + waitQueue[i](); + } + waitQueue = []; + }); + } + + return function() { + var passed = false; + waitForTick(function() { + passed = true; + }); + return function(callback) { + if (passed) { + callback(); + } else { + waitForTick(callback); + } + }; + }; + }]; + }; + + var $$AnimateRunnerFactoryProvider = /** @this */ function() { + this.$get = ['$q', '$sniffer', '$$animateAsyncRun', '$$isDocumentHidden', '$timeout', + function($q, $sniffer, $$animateAsyncRun, $$isDocumentHidden, $timeout) { + + var INITIAL_STATE = 0; + var DONE_PENDING_STATE = 1; + var DONE_COMPLETE_STATE = 2; + + AnimateRunner.chain = function(chain, callback) { + var index = 0; + + next(); + function next() { + if (index === chain.length) { + callback(true); + return; + } + + chain[index](function(response) { + if (response === false) { + callback(false); + return; + } + index++; + next(); + }); + } + }; + + AnimateRunner.all = function(runners, callback) { + var count = 0; + var status = true; + forEach(runners, function(runner) { + runner.done(onProgress); + }); + + function onProgress(response) { + status = status && response; + if (++count === runners.length) { + callback(status); + } + } + }; + + function AnimateRunner(host) { + this.setHost(host); + + var rafTick = $$animateAsyncRun(); + var timeoutTick = function(fn) { + $timeout(fn, 0, false); + }; + + this._doneCallbacks = []; + this._tick = function(fn) { + if ($$isDocumentHidden()) { + timeoutTick(fn); + } else { + rafTick(fn); + } + }; + this._state = 0; + } + + AnimateRunner.prototype = { + setHost: function(host) { + this.host = host || {}; + }, + + done: function(fn) { + if (this._state === DONE_COMPLETE_STATE) { + fn(); + } else { + this._doneCallbacks.push(fn); + } + }, + + progress: noop, + + getPromise: function() { + if (!this.promise) { + var self = this; + this.promise = $q(function(resolve, reject) { + self.done(function(status) { + if (status === false) { + reject(); + } else { + resolve(); + } + }); + }); + } + return this.promise; + }, + + then: function(resolveHandler, rejectHandler) { + return this.getPromise().then(resolveHandler, rejectHandler); + }, + + 'catch': function(handler) { + return this.getPromise()['catch'](handler); + }, + + 'finally': function(handler) { + return this.getPromise()['finally'](handler); + }, + + pause: function() { + if (this.host.pause) { + this.host.pause(); + } + }, + + resume: function() { + if (this.host.resume) { + this.host.resume(); + } + }, + + end: function() { + if (this.host.end) { + this.host.end(); + } + this._resolve(true); + }, + + cancel: function() { + if (this.host.cancel) { + this.host.cancel(); + } + this._resolve(false); + }, + + complete: function(response) { + var self = this; + if (self._state === INITIAL_STATE) { + self._state = DONE_PENDING_STATE; + self._tick(function() { + self._resolve(response); + }); + } + }, + + _resolve: function(response) { + if (this._state !== DONE_COMPLETE_STATE) { + forEach(this._doneCallbacks, function(fn) { + fn(response); + }); + this._doneCallbacks.length = 0; + this._state = DONE_COMPLETE_STATE; + } + } + }; + + return AnimateRunner; + }]; + }; + + /* exported $CoreAnimateCssProvider */ + + /** + * @ngdoc service + * @name $animateCss + * @kind object + * @this + * + * @description + * This is the core version of `$animateCss`. By default, only when the `ngAnimate` is included, + * then the `$animateCss` service will actually perform animations. + * + * Click here {@link ngAnimate.$animateCss to read the documentation for $animateCss}. + */ + var $CoreAnimateCssProvider = function() { + this.$get = ['$$rAF', '$q', '$$AnimateRunner', function($$rAF, $q, $$AnimateRunner) { + + return function(element, initialOptions) { + // all of the animation functions should create + // a copy of the options data, however, if a + // parent service has already created a copy then + // we should stick to using that + var options = initialOptions || {}; + if (!options.$$prepared) { + options = copy(options); + } + + // there is no point in applying the styles since + // there is no animation that goes on at all in + // this version of $animateCss. + if (options.cleanupStyles) { + options.from = options.to = null; + } + + if (options.from) { + element.css(options.from); + options.from = null; + } + + var closed, runner = new $$AnimateRunner(); + return { + start: run, + end: run + }; + + function run() { + $$rAF(function() { + applyAnimationContents(); + if (!closed) { + runner.complete(); + } + closed = true; + }); + return runner; + } + + function applyAnimationContents() { + if (options.addClass) { + element.addClass(options.addClass); + options.addClass = null; + } + if (options.removeClass) { + element.removeClass(options.removeClass); + options.removeClass = null; + } + if (options.to) { + element.css(options.to); + options.to = null; + } + } + }; + }]; + }; + + /* global stripHash: true */ + /** * ! This is a private undocumented service ! * - * @name ng.$browser + * @name $browser * @requires $log * @description * This object has two goals: @@ -4074,18 +6333,16 @@ /** * @param {object} window The global window object. * @param {object} document jQuery wrapped document. - * @param {function()} XHR XMLHttpRequest constructor. - * @param {object} $log console.log or an object with the same interface. + * @param {object} $log window.console or an object with the same interface. * @param {object} $sniffer $sniffer service */ function Browser(window, document, $log, $sniffer) { var self = this, - rawDocument = document[0], - location = window.location, - history = window.history, - setTimeout = window.setTimeout, - clearTimeout = window.clearTimeout, - pendingDeferIds = {}; + location = window.location, + history = window.history, + setTimeout = window.setTimeout, + clearTimeout = window.clearTimeout, + pendingDeferIds = {}; self.isMock = false; @@ -4106,7 +6363,7 @@ } finally { outstandingRequestCount--; if (outstandingRequestCount === 0) { - while(outstandingRequestCallbacks.length) { + while (outstandingRequestCallbacks.length) { try { outstandingRequestCallbacks.pop()(); } catch (e) { @@ -4117,6 +6374,11 @@ } } + function getHash(url) { + var index = url.indexOf('#'); + return index === -1 ? '' : url.substr(index); + } + /** * @private * Note: this method is used only by scenario runner @@ -4124,11 +6386,6 @@ * @param {function()} callback Function that will be called when no outstanding request */ self.notifyWhenNoOutstandingRequests = function(callback) { - // force browser to execute all pollFns - this is needed so that cookies and other pollers fire - // at some deterministic time in respect to the test runner's actions. Leaving things up to the - // regular poller would result in flaky tests. - forEach(pollFns, function(pollFn){ pollFn(); }); - if (outstandingRequestCount === 0) { callback(); } else { @@ -4136,56 +6393,26 @@ } }; - ////////////////////////////////////////////////////////////// - // Poll Watcher API - ////////////////////////////////////////////////////////////// - var pollFns = [], - pollTimeout; - - /** - * @name ng.$browser#addPollFn - * @methodOf ng.$browser - * - * @param {function()} fn Poll function to add - * - * @description - * Adds a function to the list of functions that poller periodically executes, - * and starts polling if not started yet. - * - * @returns {function()} the added function - */ - self.addPollFn = function(fn) { - if (isUndefined(pollTimeout)) startPoller(100, setTimeout); - pollFns.push(fn); - return fn; - }; - - /** - * @param {number} interval How often should browser call poll functions (ms) - * @param {function()} setTimeout Reference to a real or fake `setTimeout` function. - * - * @description - * Configures the poller to run in the specified intervals, using the specified - * setTimeout fn and kicks it off. - */ - function startPoller(interval, setTimeout) { - (function check() { - forEach(pollFns, function(pollFn){ pollFn(); }); - pollTimeout = setTimeout(check, interval); - })(); - } - ////////////////////////////////////////////////////////////// // URL API ////////////////////////////////////////////////////////////// - var lastBrowserUrl = location.href, - baseElement = document.find('base'), - newLocation = null; + var cachedState, lastHistoryState, + lastBrowserUrl = location.href, + baseElement = document.find('base'), + pendingLocation = null, + getCurrentState = !$sniffer.history ? noop : function getCurrentState() { + try { + return history.state; + } catch (e) { + // MSIE can reportedly throw when there is no state (UNCONFIRMED). + } + }; + + cacheState(); /** - * @name ng.$browser#url - * @methodOf ng.$browser + * @name $browser#url * * @description * GETTER: @@ -4201,59 +6428,125 @@ * {@link ng.$location $location service} to change url. * * @param {string} url New url (when used as setter) - * @param {boolean=} replace Should new url replace current history record ? + * @param {boolean=} replace Should new url replace current history record? + * @param {object=} state object to use with pushState/replaceState */ - self.url = function(url, replace) { + self.url = function(url, replace, state) { + // In modern browsers `history.state` is `null` by default; treating it separately + // from `undefined` would cause `$browser.url('/foo')` to change `history.state` + // to undefined via `pushState`. Instead, let's change `undefined` to `null` here. + if (isUndefined(state)) { + state = null; + } + // Android Browser BFCache causes location, history reference to become stale. if (location !== window.location) location = window.location; if (history !== window.history) history = window.history; // setter if (url) { - if (lastBrowserUrl == url) return; + var sameState = lastHistoryState === state; + + // Don't change anything if previous and current URLs and states match. This also prevents + // IE<10 from getting into redirect loop when in LocationHashbangInHtml5Url mode. + // See https://github.com/angular/angular.js/commit/ffb2701 + if (lastBrowserUrl === url && (!$sniffer.history || sameState)) { + return self; + } + var sameBase = lastBrowserUrl && stripHash(lastBrowserUrl) === stripHash(url); lastBrowserUrl = url; - if ($sniffer.history) { - if (replace) history.replaceState(null, '', url); - else { - history.pushState(null, '', url); - // Crazy Opera Bug: http://my.opera.com/community/forums/topic.dml?id=1185462 - baseElement.attr('href', baseElement.attr('href')); - } + lastHistoryState = state; + // Don't use history API if only the hash changed + // due to a bug in IE10/IE11 which leads + // to not firing a `hashchange` nor `popstate` event + // in some cases (see #9143). + if ($sniffer.history && (!sameBase || !sameState)) { + history[replace ? 'replaceState' : 'pushState'](state, '', url); + cacheState(); } else { - newLocation = url; + if (!sameBase) { + pendingLocation = url; + } if (replace) { location.replace(url); - } else { + } else if (!sameBase) { location.href = url; + } else { + location.hash = getHash(url); } + if (location.href !== url) { + pendingLocation = url; + } + } + if (pendingLocation) { + pendingLocation = url; } return self; // getter } else { - // - newLocation is a workaround for an IE7-9 issue with location.replace and location.href - // methods not updating location.href synchronously. + // - pendingLocation is needed as browsers don't allow to read out + // the new location.href if a reload happened or if there is a bug like in iOS 9 (see + // https://openradar.appspot.com/22186109). // - the replacement is a workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=407172 - return newLocation || location.href.replace(/%27/g,"'"); + return pendingLocation || location.href.replace(/%27/g,'\''); } }; - var urlChangeListeners = [], - urlChangeInit = false; + /** + * @name $browser#state + * + * @description + * This method is a getter. + * + * Return history.state or null if history.state is undefined. + * + * @returns {object} state + */ + self.state = function() { + return cachedState; + }; - function fireUrlChange() { - newLocation = null; - if (lastBrowserUrl == self.url()) return; + var urlChangeListeners = [], + urlChangeInit = false; + + function cacheStateAndFireUrlChange() { + pendingLocation = null; + fireStateOrUrlChange(); + } + + // This variable should be used *only* inside the cacheState function. + var lastCachedState = null; + function cacheState() { + // This should be the only place in $browser where `history.state` is read. + cachedState = getCurrentState(); + cachedState = isUndefined(cachedState) ? null : cachedState; + + // Prevent callbacks fo fire twice if both hashchange & popstate were fired. + if (equals(cachedState, lastCachedState)) { + cachedState = lastCachedState; + } + + lastCachedState = cachedState; + lastHistoryState = cachedState; + } + + function fireStateOrUrlChange() { + var prevLastHistoryState = lastHistoryState; + cacheState(); + + if (lastBrowserUrl === self.url() && prevLastHistoryState === cachedState) { + return; + } lastBrowserUrl = self.url(); + lastHistoryState = cachedState; forEach(urlChangeListeners, function(listener) { - listener(self.url()); + listener(self.url(), cachedState); }); } /** - * @name ng.$browser#onUrlChange - * @methodOf ng.$browser - * @TODO(vojta): refactor to use node's syntax for events + * @name $browser#onUrlChange * * @description * Register callback function that will be called, when url changes. @@ -4274,17 +6567,16 @@ * @return {function(string)} Returns the registered listener fn - handy if the fn is anonymous. */ self.onUrlChange = function(callback) { + // TODO(vojta): refactor to use node's syntax for events if (!urlChangeInit) { - // We listen on both (hashchange/popstate) when available, as some browsers (e.g. Opera) - // don't fire popstate when user change the address bar and don't fire hashchange when url + // We listen on both (hashchange/popstate) when available, as some browsers don't + // fire popstate when user changes the address bar and don't fire hashchange when url // changed by push/replaceState // html5 history api - popstate event - if ($sniffer.history) jqLite(window).on('popstate', fireUrlChange); + if ($sniffer.history) jqLite(window).on('popstate', cacheStateAndFireUrlChange); // hashchange event - if ($sniffer.hashchange) jqLite(window).on('hashchange', fireUrlChange); - // polling - else self.addPollFn(fireUrlChange); + jqLite(window).on('hashchange', cacheStateAndFireUrlChange); urlChangeInit = true; } @@ -4293,105 +6585,43 @@ return callback; }; + /** + * @private + * Remove popstate and hashchange handler from window. + * + * NOTE: this api is intended for use only by $rootScope. + */ + self.$$applicationDestroyed = function() { + jqLite(window).off('hashchange popstate', cacheStateAndFireUrlChange); + }; + + /** + * Checks whether the url has changed outside of Angular. + * Needs to be exported to be able to check for changes that have been done in sync, + * as hashchange/popstate events fire in async. + */ + self.$$checkUrlChange = fireStateOrUrlChange; + ////////////////////////////////////////////////////////////// // Misc API ////////////////////////////////////////////////////////////// /** - * @name ng.$browser#baseHref - * @methodOf ng.$browser + * @name $browser#baseHref * * @description * Returns current * (always relative - without domain) * - * @returns {string=} current + * @returns {string} The current base href */ self.baseHref = function() { var href = baseElement.attr('href'); - return href ? href.replace(/^(https?\:)?\/\/[^\/]*/, '') : ''; + return href ? href.replace(/^(https?:)?\/\/[^/]*/, '') : ''; }; - ////////////////////////////////////////////////////////////// - // Cookies API - ////////////////////////////////////////////////////////////// - var lastCookies = {}; - var lastCookieString = ''; - var cookiePath = self.baseHref(); - /** - * @name ng.$browser#cookies - * @methodOf ng.$browser - * - * @param {string=} name Cookie name - * @param {string=} value Cookie value - * - * @description - * The cookies method provides a 'private' low level access to browser cookies. - * It is not meant to be used directly, use the $cookie service instead. - * - * The return values vary depending on the arguments that the method was called with as follows: - * - * - cookies() -> hash of all cookies, this is NOT a copy of the internal state, so do not modify - * it - * - cookies(name, value) -> set name to value, if value is undefined delete the cookie - * - cookies(name) -> the same as (name, undefined) == DELETES (no one calls it right now that - * way) - * - * @returns {Object} Hash of all cookies (if called without any parameter) - */ - self.cookies = function(name, value) { - /* global escape: false, unescape: false */ - var cookieLength, cookieArray, cookie, i, index; - - if (name) { - if (value === undefined) { - rawDocument.cookie = escape(name) + "=;path=" + cookiePath + - ";expires=Thu, 01 Jan 1970 00:00:00 GMT"; - } else { - if (isString(value)) { - cookieLength = (rawDocument.cookie = escape(name) + '=' + escape(value) + - ';path=' + cookiePath).length + 1; - - // per http://www.ietf.org/rfc/rfc2109.txt browser must allow at minimum: - // - 300 cookies - // - 20 cookies per unique domain - // - 4096 bytes per cookie - if (cookieLength > 4096) { - $log.warn("Cookie '"+ name + - "' possibly not set or overflowed because it was too large ("+ - cookieLength + " > 4096 bytes)!"); - } - } - } - } else { - if (rawDocument.cookie !== lastCookieString) { - lastCookieString = rawDocument.cookie; - cookieArray = lastCookieString.split("; "); - lastCookies = {}; - - for (i = 0; i < cookieArray.length; i++) { - cookie = cookieArray[i]; - index = cookie.indexOf('='); - if (index > 0) { //ignore nameless cookies - name = unescape(cookie.substring(0, index)); - // the first value that is seen for a cookie is the most - // specific one. values for the same cookie name that - // follow are for less specific paths. - if (lastCookies[name] === undefined) { - lastCookies[name] = unescape(cookie.substring(index + 1)); - } - } - } - } - return lastCookies; - } - }; - - - /** - * @name ng.$browser#defer - * @methodOf ng.$browser + * @name $browser#defer * @param {function()} fn A function, who's execution should be deferred. * @param {number=} [delay=0] of milliseconds to defer the function execution. * @returns {*} DeferId that can be used to cancel the task via `$browser.defer.cancel()`. @@ -4417,8 +6647,7 @@ /** - * @name ng.$browser#defer.cancel - * @methodOf ng.$browser.defer + * @name $browser#defer.cancel * * @description * Cancels a deferred task identified with `deferId`. @@ -4439,21 +6668,24 @@ } - function $BrowserProvider(){ + /** @this */ + function $BrowserProvider() { this.$get = ['$window', '$log', '$sniffer', '$document', - function( $window, $log, $sniffer, $document){ + function($window, $log, $sniffer, $document) { return new Browser($window, $document, $log, $sniffer); }]; } /** - * @ngdoc object - * @name ng.$cacheFactory + * @ngdoc service + * @name $cacheFactory + * @this * * @description - * Factory that constructs cache objects and gives access to them. + * Factory that constructs {@link $cacheFactory.Cache Cache} objects and gives access to + * them. * - *
    +   * ```js
        *
        *  var cache = $cacheFactory('cacheId');
        *  expect($cacheFactory.get('cacheId')).toBe(cache);
    @@ -4465,7 +6697,7 @@
        *  // We've specified no options on creation
        *  expect(cache.info()).toEqual({id: 'cacheId', size: 2});
        *
    -   * 
    + * ``` * * * @param {string} cacheId Name or id of the newly created cache. @@ -4483,6 +6715,48 @@ * - `{void}` `removeAll()` — Removes all cached values. * - `{void}` `destroy()` — Removes references to this cache from $cacheFactory. * + * @example + + +
    + + + + +

    Cached Values

    +
    + + : + +
    + +

    Cache Info

    +
    + + : + +
    +
    +
    + + angular.module('cacheExampleApp', []). + controller('CacheController', ['$scope', '$cacheFactory', function($scope, $cacheFactory) { + $scope.keys = []; + $scope.cache = $cacheFactory('cacheId'); + $scope.put = function(key, value) { + if (angular.isUndefined($scope.cache.get(key))) { + $scope.keys.push(key); + } + $scope.cache.put(key, angular.isUndefined(value) ? null : value); + }; + }]); + + + p { + margin: 10px 0 3px; + } + +
    */ function $CacheFactoryProvider() { @@ -4491,25 +6765,84 @@ function cacheFactory(cacheId, options) { if (cacheId in caches) { - throw minErr('$cacheFactory')('iid', "CacheId '{0}' is already taken!", cacheId); + throw minErr('$cacheFactory')('iid', 'CacheId \'{0}\' is already taken!', cacheId); } var size = 0, - stats = extend({}, options, {id: cacheId}), - data = {}, - capacity = (options && options.capacity) || Number.MAX_VALUE, - lruHash = {}, - freshEnd = null, - staleEnd = null; + stats = extend({}, options, {id: cacheId}), + data = createMap(), + capacity = (options && options.capacity) || Number.MAX_VALUE, + lruHash = createMap(), + freshEnd = null, + staleEnd = null; - return caches[cacheId] = { + /** + * @ngdoc type + * @name $cacheFactory.Cache + * + * @description + * A cache object used to store and retrieve data, primarily used by + * {@link $http $http} and the {@link ng.directive:script script} directive to cache + * templates and other data. + * + * ```js + * angular.module('superCache') + * .factory('superCache', ['$cacheFactory', function($cacheFactory) { + * return $cacheFactory('super-cache'); + * }]); + * ``` + * + * Example test: + * + * ```js + * it('should behave like a cache', inject(function(superCache) { + * superCache.put('key', 'value'); + * superCache.put('another key', 'another value'); + * + * expect(superCache.info()).toEqual({ + * id: 'super-cache', + * size: 2 + * }); + * + * superCache.remove('another key'); + * expect(superCache.get('another key')).toBeUndefined(); + * + * superCache.removeAll(); + * expect(superCache.info()).toEqual({ + * id: 'super-cache', + * size: 0 + * }); + * })); + * ``` + */ + return (caches[cacheId] = { + /** + * @ngdoc method + * @name $cacheFactory.Cache#put + * @kind function + * + * @description + * Inserts a named entry into the {@link $cacheFactory.Cache Cache} object to be + * retrieved later, and incrementing the size of the cache if the key was not already + * present in the cache. If behaving like an LRU cache, it will also remove stale + * entries from the set. + * + * It will not insert undefined values into the cache. + * + * @param {string} key the key under which the cached data is stored. + * @param {*} value the value to store alongside the key. If it is undefined, the key + * will not be stored. + * @returns {*} the value stored. + */ put: function(key, value) { - var lruEntry = lruHash[key] || (lruHash[key] = {key: key}); - - refresh(lruEntry); - if (isUndefined(value)) return; + if (capacity < Number.MAX_VALUE) { + var lruEntry = lruHash[key] || (lruHash[key] = {key: key}); + + refresh(lruEntry); + } + if (!(key in data)) size++; data[key] = value; @@ -4520,41 +6853,85 @@ return value; }, - + /** + * @ngdoc method + * @name $cacheFactory.Cache#get + * @kind function + * + * @description + * Retrieves named data stored in the {@link $cacheFactory.Cache Cache} object. + * + * @param {string} key the key of the data to be retrieved + * @returns {*} the value stored. + */ get: function(key) { - var lruEntry = lruHash[key]; + if (capacity < Number.MAX_VALUE) { + var lruEntry = lruHash[key]; - if (!lruEntry) return; + if (!lruEntry) return; - refresh(lruEntry); + refresh(lruEntry); + } return data[key]; }, + /** + * @ngdoc method + * @name $cacheFactory.Cache#remove + * @kind function + * + * @description + * Removes an entry from the {@link $cacheFactory.Cache Cache} object. + * + * @param {string} key the key of the entry to be removed + */ remove: function(key) { - var lruEntry = lruHash[key]; + if (capacity < Number.MAX_VALUE) { + var lruEntry = lruHash[key]; - if (!lruEntry) return; + if (!lruEntry) return; - if (lruEntry == freshEnd) freshEnd = lruEntry.p; - if (lruEntry == staleEnd) staleEnd = lruEntry.n; - link(lruEntry.n,lruEntry.p); + if (lruEntry === freshEnd) freshEnd = lruEntry.p; + if (lruEntry === staleEnd) staleEnd = lruEntry.n; + link(lruEntry.n,lruEntry.p); + + delete lruHash[key]; + } + + if (!(key in data)) return; - delete lruHash[key]; delete data[key]; size--; }, + /** + * @ngdoc method + * @name $cacheFactory.Cache#removeAll + * @kind function + * + * @description + * Clears the cache object of any entries. + */ removeAll: function() { - data = {}; + data = createMap(); size = 0; - lruHash = {}; + lruHash = createMap(); freshEnd = staleEnd = null; }, + /** + * @ngdoc method + * @name $cacheFactory.Cache#destroy + * @kind function + * + * @description + * Destroys the {@link $cacheFactory.Cache Cache} object entirely, + * removing it from the {@link $cacheFactory $cacheFactory} set. + */ destroy: function() { data = null; stats = null; @@ -4563,20 +6940,36 @@ }, + /** + * @ngdoc method + * @name $cacheFactory.Cache#info + * @kind function + * + * @description + * Retrieve information regarding a particular {@link $cacheFactory.Cache Cache}. + * + * @returns {object} an object with the following properties: + *
      + *
    • **id**: the id of the cache instance
    • + *
    • **size**: the number of entries kept in the cache instance
    • + *
    • **...**: any additional properties from the options object when creating the + * cache.
    • + *
    + */ info: function() { return extend({}, stats, {size: size}); } - }; + }); /** * makes the `entry` the freshEnd of the LRU linked list */ function refresh(entry) { - if (entry != freshEnd) { + if (entry !== freshEnd) { if (!staleEnd) { staleEnd = entry; - } else if (staleEnd == entry) { + } else if (staleEnd === entry) { staleEnd = entry.n; } @@ -4592,7 +6985,7 @@ * bidirectionally links two entries of the LRU linked list */ function link(nextEntry, prevEntry) { - if (nextEntry != prevEntry) { + if (nextEntry !== prevEntry) { if (nextEntry) nextEntry.p = prevEntry; //p stands for previous, 'prev' didn't minify if (prevEntry) prevEntry.n = nextEntry; //n stands for next, 'next' didn't minify } @@ -4602,11 +6995,10 @@ /** * @ngdoc method - * @name ng.$cacheFactory#info - * @methodOf ng.$cacheFactory + * @name $cacheFactory#info * * @description - * Get information about all the of the caches that have been created + * Get information about all the caches that have been created * * @returns {Object} - key-value map of `cacheId` to the result of calling `cache#info` */ @@ -4621,8 +7013,7 @@ /** * @ngdoc method - * @name ng.$cacheFactory#get - * @methodOf ng.$cacheFactory + * @name $cacheFactory#get * * @description * Get access to a cache object by the `cacheId` used when it was created. @@ -4640,8 +7031,9 @@ } /** - * @ngdoc object - * @name ng.$templateCache + * @ngdoc service + * @name $templateCache + * @this * * @description * The first time a template is used, it is loaded in the template cache for quick retrieval. You @@ -4649,38 +7041,37 @@ * `$templateCache` service directly. * * Adding via the `script` tag: - *
    -   * 
    -   * 
    -   * 
    -   * 
    -   *   ...
    -   * 
    -   * 
    + * + * ```html + * + * ``` * * **Note:** the `script` tag containing the template does not need to be included in the `head` of - * the document, but it must be below the `ng-app` definition. + * the document, but it must be a descendent of the {@link ng.$rootElement $rootElement} (IE, + * element with ng-app attribute), otherwise the template will be ignored. * - * Adding via the $templateCache service: + * Adding via the `$templateCache` service: * - *
    +   * ```js
        * var myApp = angular.module('myApp', []);
        * myApp.run(function($templateCache) {
      *   $templateCache.put('templateId.html', 'This is the content of the template');
      * });
    -   * 
    + * ``` * - * To retrieve the template later, simply use it in your HTML: - *
    -   * 
    - *
    + * To retrieve the template later, simply use it in your component: + * ```js + * myApp.component('myComponent', { + * templateUrl: 'templateId.html' + * }); + * ``` * - * or get it via Javascript: - *
    +   * or get it via the `$templateCache` service:
    +   * ```js
        * $templateCache.get('templateId.html')
    -   * 
    + * ``` * * See {@link ng.$cacheFactory $cacheFactory}. * @@ -4691,35 +7082,46 @@ }]; } + /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * Any commits to this file should be reviewed with security in mind. * + * Changes to this file can potentially create security vulnerabilities. * + * An approval from 2 Core members with history of modifying * + * this file is required. * + * * + * Does the change somehow allow for arbitrary javascript to be executed? * + * Or allows for someone to change the prototype of built-in objects? * + * Or gives undesired access to variables like document or window? * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + /* ! VARIABLE/FUNCTION NAMING CONVENTIONS THAT APPLY TO THIS FILE! - * - * DOM-related variables: - * - * - "node" - DOM Node - * - "element" - DOM Element or Node - * - "$node" or "$element" - jqLite-wrapped node or element - * - * - * Compiler related stuff: - * - * - "linkFn" - linking fn of a single directive - * - "nodeLinkFn" - function that aggregates all linking fns for a particular node - * - "childLinkFn" - function that aggregates all linking fns for child nodes of a particular node - * - "compositeLinkFn" - function that aggregates all linking fns for a compilation root (nodeList) - */ + * + * DOM-related variables: + * + * - "node" - DOM Node + * - "element" - DOM Element or Node + * - "$node" or "$element" - jqLite-wrapped node or element + * + * + * Compiler related stuff: + * + * - "linkFn" - linking fn of a single directive + * - "nodeLinkFn" - function that aggregates all linking fns for a particular node + * - "childLinkFn" - function that aggregates all linking fns for child nodes of a particular node + * - "compositeLinkFn" - function that aggregates all linking fns for a compilation root (nodeList) + */ /** - * @ngdoc function - * @name ng.$compile - * @function + * @ngdoc service + * @name $compile + * @kind function * * @description * Compiles an HTML string or DOM into a template and produces a template function, which * can then be used to link {@link ng.$rootScope.Scope `scope`} and the template together. * * The compilation is a process of walking the DOM tree and matching DOM elements to - * {@link ng.$compileProvider#methods_directive directives}. + * {@link ng.$compileProvider#directive directives}. * *
    * **Note:** This document is an in-depth reference of all directive options. @@ -4732,8 +7134,9 @@ * There are many different options for a directive. * * The difference resides in the return value of the factory function. - * You can either return a "Directive Definition Object" (see below) that defines the directive properties, - * or just the `postLink` function (all other properties will have the default values). + * You can either return a {@link $compile#directive-definition-object Directive Definition Object (see below)} + * that defines the directive properties, or just the `postLink` function (all other properties will have + * the default values). * *
    * **Best Practice:** It's recommended to use the "directive definition object" form. @@ -4741,40 +7144,43 @@ * * Here's an example directive declared with a Directive Definition Object: * - *
    +   * ```js
        *   var myModule = angular.module(...);
        *
        *   myModule.directive('directiveName', function factory(injectables) {
      *     var directiveDefinitionObject = {
    - *       priority: 0,
    - *       template: '
    ', // or // function(tElement, tAttrs) { ... }, + * {@link $compile#-priority- priority}: 0, + * {@link $compile#-template- template}: '
    ', // or // function(tElement, tAttrs) { ... }, * // or - * // templateUrl: 'directive.html', // or // function(tElement, tAttrs) { ... }, - * replace: false, - * transclude: false, - * restrict: 'A', - * scope: false, - * controller: function($scope, $element, $attrs, $transclude, otherInjectables) { ... }, - * require: 'siblingDirectiveName', // or // ['^parentDirectiveName', '?optionalDirectiveName', '?^optionalParent'], - * compile: function compile(tElement, tAttrs, transclude) { + * // {@link $compile#-templateurl- templateUrl}: 'directive.html', // or // function(tElement, tAttrs) { ... }, + * {@link $compile#-transclude- transclude}: false, + * {@link $compile#-restrict- restrict}: 'A', + * {@link $compile#-templatenamespace- templateNamespace}: 'html', + * {@link $compile#-scope- scope}: false, + * {@link $compile#-controller- controller}: function($scope, $element, $attrs, $transclude, otherInjectables) { ... }, + * {@link $compile#-controlleras- controllerAs}: 'stringIdentifier', + * {@link $compile#-bindtocontroller- bindToController}: false, + * {@link $compile#-require- require}: 'siblingDirectiveName', // or // ['^parentDirectiveName', '?optionalDirectiveName', '?^optionalParent'], + * {@link $compile#-multielement- multiElement}: false, + * {@link $compile#-compile- compile}: function compile(tElement, tAttrs, transclude) { * return { - * pre: function preLink(scope, iElement, iAttrs, controller) { ... }, - * post: function postLink(scope, iElement, iAttrs, controller) { ... } + * {@link $compile#pre-linking-function pre}: function preLink(scope, iElement, iAttrs, controller) { ... }, + * {@link $compile#post-linking-function post}: function postLink(scope, iElement, iAttrs, controller) { ... } * } * // or * // return function postLink( ... ) { ... } * }, * // or - * // link: { - * // pre: function preLink(scope, iElement, iAttrs, controller) { ... }, - * // post: function postLink(scope, iElement, iAttrs, controller) { ... } + * // {@link $compile#-link- link}: { + * // {@link $compile#pre-linking-function pre}: function preLink(scope, iElement, iAttrs, controller) { ... }, + * // {@link $compile#post-linking-function post}: function postLink(scope, iElement, iAttrs, controller) { ... } * // } * // or - * // link: function postLink( ... ) { ... } + * // {@link $compile#-link- link}: function postLink( ... ) { ... } * }; * return directiveDefinitionObject; * }); - *
    + * ``` * *
    * **Note:** Any unspecified options will use the default value. You can see the default values below. @@ -4782,7 +7188,7 @@ * * Therefore the above can be simplified as: * - *
    +   * ```js
        *   var myModule = angular.module(...);
        *
        *   myModule.directive('directiveName', function factory(injectables) {
    @@ -4793,14 +7199,141 @@
      *     // or
      *     // return function postLink(scope, iElement, iAttrs) { ... }
      *   });
    -   * 
    + * ``` * + * ### Life-cycle hooks + * Directive controllers can provide the following methods that are called by Angular at points in the life-cycle of the + * directive: + * * `$onInit()` - Called on each controller after all the controllers on an element have been constructed and + * had their bindings initialized (and before the pre & post linking functions for the directives on + * this element). This is a good place to put initialization code for your controller. + * * `$onChanges(changesObj)` - Called whenever one-way (`<`) or interpolation (`@`) bindings are updated. The + * `changesObj` is a hash whose keys are the names of the bound properties that have changed, and the values are an + * object of the form `{ currentValue, previousValue, isFirstChange() }`. Use this hook to trigger updates within a + * component such as cloning the bound value to prevent accidental mutation of the outer value. Note that this will + * also be called when your bindings are initialized. + * * `$doCheck()` - Called on each turn of the digest cycle. Provides an opportunity to detect and act on + * changes. Any actions that you wish to take in response to the changes that you detect must be + * invoked from this hook; implementing this has no effect on when `$onChanges` is called. For example, this hook + * could be useful if you wish to perform a deep equality check, or to check a Date object, changes to which would not + * be detected by Angular's change detector and thus not trigger `$onChanges`. This hook is invoked with no arguments; + * if detecting changes, you must store the previous value(s) for comparison to the current values. + * * `$onDestroy()` - Called on a controller when its containing scope is destroyed. Use this hook for releasing + * external resources, watches and event handlers. Note that components have their `$onDestroy()` hooks called in + * the same order as the `$scope.$broadcast` events are triggered, which is top down. This means that parent + * components will have their `$onDestroy()` hook called before child components. + * * `$postLink()` - Called after this controller's element and its children have been linked. Similar to the post-link + * function this hook can be used to set up DOM event handlers and do direct DOM manipulation. + * Note that child elements that contain `templateUrl` directives will not have been compiled and linked since + * they are waiting for their template to load asynchronously and their own compilation and linking has been + * suspended until that occurs. + * + * #### Comparison with Angular 2 life-cycle hooks + * Angular 2 also uses life-cycle hooks for its components. While the Angular 1 life-cycle hooks are similar there are + * some differences that you should be aware of, especially when it comes to moving your code from Angular 1 to Angular 2: + * + * * Angular 1 hooks are prefixed with `$`, such as `$onInit`. Angular 2 hooks are prefixed with `ng`, such as `ngOnInit`. + * * Angular 1 hooks can be defined on the controller prototype or added to the controller inside its constructor. + * In Angular 2 you can only define hooks on the prototype of the Component class. + * * Due to the differences in change-detection, you may get many more calls to `$doCheck` in Angular 1 than you would to + * `ngDoCheck` in Angular 2 + * * Changes to the model inside `$doCheck` will trigger new turns of the digest loop, which will cause the changes to be + * propagated throughout the application. + * Angular 2 does not allow the `ngDoCheck` hook to trigger a change outside of the component. It will either throw an + * error or do nothing depending upon the state of `enableProdMode()`. + * + * #### Life-cycle hook examples + * + * This example shows how you can check for mutations to a Date object even though the identity of the object + * has not changed. + * + * + * + * angular.module('do-check-module', []) + * .component('app', { + * template: + * 'Month: ' + + * 'Date: {{ $ctrl.date }}' + + * '', + * controller: function() { + * this.date = new Date(); + * this.month = this.date.getMonth(); + * this.updateDate = function() { + * this.date.setMonth(this.month); + * }; + * } + * }) + * .component('test', { + * bindings: { date: '<' }, + * template: + * '
    {{ $ctrl.log | json }}
    ', + * controller: function() { + * var previousValue; + * this.log = []; + * this.$doCheck = function() { + * var currentValue = this.date && this.date.valueOf(); + * if (previousValue !== currentValue) { + * this.log.push('doCheck: date mutated: ' + this.date); + * previousValue = currentValue; + * } + * }; + * } + * }); + *
    + * + * + * + *
    + * + * This example show how you might use `$doCheck` to trigger changes in your component's inputs even if the + * actual identity of the component doesn't change. (Be aware that cloning and deep equality checks on large + * arrays or objects can have a negative impact on your application performance) + * + * + * + *
    + * + * + *
    {{ items }}
    + * + *
    + *
    + * + * angular.module('do-check-module', []) + * .component('test', { + * bindings: { items: '<' }, + * template: + * '
    {{ $ctrl.log | json }}
    ', + * controller: function() { + * this.log = []; + * + * this.$doCheck = function() { + * if (this.items_ref !== this.items) { + * this.log.push('doCheck: items changed'); + * this.items_ref = this.items; + * } + * if (!angular.equals(this.items_clone, this.items)) { + * this.log.push('doCheck: items mutated'); + * this.items_clone = angular.copy(this.items); + * } + * }; + * } + * }); + *
    + *
    * * * ### Directive Definition Object * - * The directive definition object provides instructions to the {@link ../api/ng.$compile - * compiler}. The attributes are: + * The directive definition object provides instructions to the {@link ng.$compile + * compiler}. The attributes are: + * + * #### `multiElement` + * When this property is set to true (default is `false`), the HTML compiler will collect DOM nodes between + * nodes with the attributes `directive-name-start` and `directive-name-end`, and group them + * together as the directive elements. It is recommended that this feature be used on directives + * which are not strictly behavioral (such as {@link ngClick}), and which + * do not manipulate or replace child nodes (such as {@link ngInclude}). * * #### `priority` * When there are multiple directives defined on a single DOM element, sometimes it @@ -4813,150 +7346,276 @@ * #### `terminal` * If set to true then the current `priority` will be the last set of directives * which will execute (any directives at the current priority will still execute - * as the order of execution on same `priority` is undefined). + * as the order of execution on same `priority` is undefined). Note that expressions + * and other directives used in the directive's template will also be excluded from execution. * * #### `scope` - * **If set to `true`,** then a new scope will be created for this directive. If multiple directives on the - * same element request a new scope, only one new scope is created. The new scope rule does not - * apply for the root of the template since the root of the template always gets a new scope. + * The scope property can be `false`, `true`, or an object: * - * **If set to `{}` (object hash),** then a new "isolate" scope is created. The 'isolate' scope differs from - * normal scope in that it does not prototypically inherit from the parent scope. This is useful - * when creating reusable components, which should not accidentally read or modify data in the - * parent scope. + * * **`false` (default):** No scope will be created for the directive. The directive will use its + * parent's scope. * - * The 'isolate' scope takes an object hash which defines a set of local scope properties - * derived from the parent scope. These local properties are useful for aliasing values for - * templates. Locals definition is a hash of local scope property to its source: + * * **`true`:** A new child scope that prototypically inherits from its parent will be created for + * the directive's element. If multiple directives on the same element request a new scope, + * only one new scope is created. + * + * * **`{...}` (an object hash):** A new "isolate" scope is created for the directive's template. + * The 'isolate' scope differs from normal scope in that it does not prototypically + * inherit from its parent scope. This is useful when creating reusable components, which should not + * accidentally read or modify data in the parent scope. Note that an isolate scope + * directive without a `template` or `templateUrl` will not apply the isolate scope + * to its children elements. + * + * The 'isolate' scope object hash defines a set of local scope properties derived from attributes on the + * directive's element. These local properties are useful for aliasing values for templates. The keys in + * the object hash map to the name of the property on the isolate scope; the values define how the property + * is bound to the parent scope, via matching attributes on the directive's element: * * * `@` or `@attr` - bind a local scope property to the value of DOM attribute. The result is - * always a string since DOM attributes are strings. If no `attr` name is specified then the - * attribute name is assumed to be the same as the local name. - * Given `` and widget definition - * of `scope: { localName:'@myAttr' }`, then widget scope property `localName` will reflect - * the interpolated value of `hello {{name}}`. As the `name` attribute changes so will the - * `localName` property on the widget scope. The `name` is read from the parent scope (not - * component scope). + * always a string since DOM attributes are strings. If no `attr` name is specified then the + * attribute name is assumed to be the same as the local name. Given `` and the isolate scope definition `scope: { localName:'@myAttr' }`, + * the directive's scope property `localName` will reflect the interpolated value of `hello + * {{name}}`. As the `name` attribute changes so will the `localName` property on the directive's + * scope. The `name` is read from the parent scope (not the directive's scope). * - * * `=` or `=attr` - set up bi-directional binding between a local scope property and the - * parent scope property of name defined via the value of the `attr` attribute. If no `attr` - * name is specified then the attribute name is assumed to be the same as the local name. - * Given `` and widget definition of - * `scope: { localModel:'=myAttr' }`, then widget scope property `localModel` will reflect the + * * `=` or `=attr` - set up a bidirectional binding between a local scope property and an expression + * passed via the attribute `attr`. The expression is evaluated in the context of the parent scope. + * If no `attr` name is specified then the attribute name is assumed to be the same as the local + * name. Given `` and the isolate scope definition `scope: { + * localModel: '=myAttr' }`, the property `localModel` on the directive's scope will reflect the + * value of `parentModel` on the parent scope. Changes to `parentModel` will be reflected in + * `localModel` and vice versa. Optional attributes should be marked as such with a question mark: + * `=?` or `=?attr`. If the binding expression is non-assignable, or if the attribute isn't + * optional and doesn't exist, an exception ({@link error/$compile/nonassign `$compile:nonassign`}) + * will be thrown upon discovering changes to the local value, since it will be impossible to sync + * them back to the parent scope. By default, the {@link ng.$rootScope.Scope#$watch `$watch`} + * method is used for tracking changes, and the equality check is based on object identity. + * However, if an object literal or an array literal is passed as the binding expression, the + * equality check is done by value (using the {@link angular.equals} function). It's also possible + * to watch the evaluated value shallowly with {@link ng.$rootScope.Scope#$watchCollection + * `$watchCollection`}: use `=*` or `=*attr` (`=*?` or `=*?attr` if the attribute is optional). + * + * * `<` or `` and directive definition of + * `scope: { localModel:'` and widget definition of - * `scope: { localFn:'&myAttr' }`, then isolate scope property `localFn` will point to - * a function wrapper for the `count = count + value` expression. Often it's desirable to - * pass data from the isolated scope via an expression and to the parent scope, this can be - * done by passing a map of local variable names and values into the expression wrapper fn. - * For example, if the expression is `increment(amount)` then we can specify the amount value - * by calling the `localFn` as `localFn({amount: 22})`. + * One-way binding is useful if you do not plan to propagate changes to your isolated scope bindings + * back to the parent. However, it does not make this completely impossible. * + * * `&` or `&attr` - provides a way to execute an expression in the context of the parent scope. If + * no `attr` name is specified then the attribute name is assumed to be the same as the local name. + * Given `` and the isolate scope definition `scope: { + * localFn:'&myAttr' }`, the isolate scope property `localFn` will point to a function wrapper for + * the `count = count + value` expression. Often it's desirable to pass data from the isolated scope + * via an expression to the parent scope. This can be done by passing a map of local variable names + * and values into the expression wrapper fn. For example, if the expression is `increment(amount)` + * then we can specify the amount value by calling the `localFn` as `localFn({amount: 22})`. + * + * In general it's possible to apply more than one directive to one element, but there might be limitations + * depending on the type of scope required by the directives. The following points will help explain these limitations. + * For simplicity only two directives are taken into account, but it is also applicable for several directives: + * + * * **no scope** + **no scope** => Two directives which don't require their own scope will use their parent's scope + * * **child scope** + **no scope** => Both directives will share one single child scope + * * **child scope** + **child scope** => Both directives will share one single child scope + * * **isolated scope** + **no scope** => The isolated directive will use it's own created isolated scope. The other directive will use + * its parent's scope + * * **isolated scope** + **child scope** => **Won't work!** Only one scope can be related to one element. Therefore these directives cannot + * be applied to the same element. + * * **isolated scope** + **isolated scope** => **Won't work!** Only one scope can be related to one element. Therefore these directives + * cannot be applied to the same element. + * + * + * #### `bindToController` + * This property is used to bind scope properties directly to the controller. It can be either + * `true` or an object hash with the same format as the `scope` property. + * + * When an isolate scope is used for a directive (see above), `bindToController: true` will + * allow a component to have its properties bound to the controller, rather than to scope. + * + * After the controller is instantiated, the initial values of the isolate scope bindings will be bound to the controller + * properties. You can access these bindings once they have been initialized by providing a controller method called + * `$onInit`, which is called after all the controllers on an element have been constructed and had their bindings + * initialized. + * + *
    + * **Deprecation warning:** if `$compileProcvider.preAssignBindingsEnabled(true)` was called, bindings for non-ES6 class + * controllers are bound to `this` before the controller constructor is called but this use is now deprecated. Please + * place initialization code that relies upon bindings inside a `$onInit` method on the controller, instead. + *
    + * + * It is also possible to set `bindToController` to an object hash with the same format as the `scope` property. + * This will set up the scope bindings to the controller directly. Note that `scope` can still be used + * to define which kind of scope is created. By default, no scope is created. Use `scope: {}` to create an isolate + * scope (useful for component directives). + * + * If both `bindToController` and `scope` are defined and have object hashes, `bindToController` overrides `scope`. * * * #### `controller` * Controller constructor function. The controller is instantiated before the - * pre-linking phase and it is shared with other directives (see + * pre-linking phase and can be accessed by other directives (see * `require` attribute). This allows the directives to communicate with each other and augment * each other's behavior. The controller is injectable (and supports bracket notation) with the following locals: * * * `$scope` - Current scope associated with the element * * `$element` - Current element * * `$attrs` - Current attributes object for the element - * * `$transclude` - A transclude linking function pre-bound to the correct transclusion scope. - * The scope can be overridden by an optional first argument. - * `function([scope], cloneLinkingFn)`. - * + * * `$transclude` - A transclude linking function pre-bound to the correct transclusion scope: + * `function([scope], cloneLinkingFn, futureParentElement, slotName)`: + * * `scope`: (optional) override the scope. + * * `cloneLinkingFn`: (optional) argument to create clones of the original transcluded content. + * * `futureParentElement` (optional): + * * defines the parent to which the `cloneLinkingFn` will add the cloned elements. + * * default: `$element.parent()` resp. `$element` for `transclude:'element'` resp. `transclude:true`. + * * only needed for transcludes that are allowed to contain non html elements (e.g. SVG elements) + * and when the `cloneLinkingFn` is passed, + * as those elements need to created and cloned in a special way when they are defined outside their + * usual containers (e.g. like ``). + * * See also the `directive.templateNamespace` property. + * * `slotName`: (optional) the name of the slot to transclude. If falsy (e.g. `null`, `undefined` or `''`) + * then the default transclusion is provided. + * The `$transclude` function also has a method on it, `$transclude.isSlotFilled(slotName)`, which returns + * `true` if the specified slot contains content (i.e. one or more DOM nodes). * * #### `require` * Require another directive and inject its controller as the fourth argument to the linking function. The - * `require` takes a string name (or array of strings) of the directive(s) to pass in. If an array is used, the - * injected argument will be an array in corresponding order. If no such directive can be - * found, or if the directive does not have a controller, then an error is raised. The name can be prefixed with: + * `require` property can be a string, an array or an object: + * * a **string** containing the name of the directive to pass to the linking function + * * an **array** containing the names of directives to pass to the linking function. The argument passed to the + * linking function will be an array of controllers in the same order as the names in the `require` property + * * an **object** whose property values are the names of the directives to pass to the linking function. The argument + * passed to the linking function will also be an object with matching keys, whose values will hold the corresponding + * controllers. + * + * If the `require` property is an object and `bindToController` is truthy, then the required controllers are + * bound to the controller using the keys of the `require` property. This binding occurs after all the controllers + * have been constructed but before `$onInit` is called. + * If the name of the required controller is the same as the local name (the key), the name can be + * omitted. For example, `{parentDir: '^^'}` is equivalent to `{parentDir: '^^parentDir'}`. + * See the {@link $compileProvider#component} helper for an example of how this can be used. + * If no such required directive(s) can be found, or if the directive does not have a controller, then an error is + * raised (unless no link function is specified and the required controllers are not being bound to the directive + * controller, in which case error checking is skipped). The name can be prefixed with: * * * (no prefix) - Locate the required controller on the current element. Throw an error if not found. * * `?` - Attempt to locate the required controller or pass `null` to the `link` fn if not found. - * * `^` - Locate the required controller by searching the element's parents. Throw an error if not found. - * * `?^` - Attempt to locate the required controller by searching the element's parents or pass `null` to the - * `link` fn if not found. + * * `^` - Locate the required controller by searching the element and its parents. Throw an error if not found. + * * `^^` - Locate the required controller by searching the element's parents. Throw an error if not found. + * * `?^` - Attempt to locate the required controller by searching the element and its parents or pass + * `null` to the `link` fn if not found. + * * `?^^` - Attempt to locate the required controller by searching the element's parents, or pass + * `null` to the `link` fn if not found. * * * #### `controllerAs` - * Controller alias at the directive scope. An alias for the controller so it - * can be referenced at the directive template. The directive needs to define a scope for this - * configuration to be used. Useful in the case when directive is used as component. + * Identifier name for a reference to the controller in the directive's scope. + * This allows the controller to be referenced from the directive template. This is especially + * useful when a directive is used as component, i.e. with an `isolate` scope. It's also possible + * to use it in a directive without an `isolate` / `new` scope, but you need to be aware that the + * `controllerAs` reference might overwrite a property that already exists on the parent scope. * * * #### `restrict` * String of subset of `EACM` which restricts the directive to a specific directive - * declaration style. If omitted, the default (attributes only) is used. + * declaration style. If omitted, the defaults (elements and attributes) are used. * - * * `E` - Element name: `` + * * `E` - Element name (default): `` * * `A` - Attribute (default): `
    ` * * `C` - Class: `
    ` * * `M` - Comment: `` * * - * #### `template` - * replace the current element with the contents of the HTML. The replacement process - * migrates all of the attributes / classes from the old element to the new one. See the - * {@link guide/directive#creating-custom-directives_creating-directives_template-expanding-directive - * Directives Guide} for an example. + * #### `templateNamespace` + * String representing the document type used by the markup in the template. + * AngularJS needs this information as those elements need to be created and cloned + * in a special way when they are defined outside their usual containers like `` and ``. * - * You can specify `template` as a string representing the template or as a function which takes - * two arguments `tElement` and `tAttrs` (described in the `compile` function api below) and - * returns a string value representing the template. + * * `html` - All root nodes in the template are HTML. Root nodes may also be + * top-level elements such as `` or ``. + * * `svg` - The root nodes in the template are SVG elements (excluding ``). + * * `math` - The root nodes in the template are MathML elements (excluding ``). + * + * If no `templateNamespace` is specified, then the namespace is considered to be `html`. + * + * #### `template` + * HTML markup that may: + * * Replace the contents of the directive's element (default). + * * Replace the directive's element itself (if `replace` is true - DEPRECATED). + * * Wrap the contents of the directive's element (if `transclude` is true). + * + * Value may be: + * + * * A string. For example `
    {{delete_str}}
    `. + * * A function which takes two arguments `tElement` and `tAttrs` (described in the `compile` + * function api below) and returns a string value. * * * #### `templateUrl` - * Same as `template` but the template is loaded from the specified URL. Because - * the template loading is asynchronous the compilation/linking is suspended until the template - * is loaded. + * This is similar to `template` but the template is loaded from the specified URL, asynchronously. + * + * Because template loading is asynchronous the compiler will suspend compilation of directives on that element + * for later when the template has been resolved. In the meantime it will continue to compile and link + * sibling and parent elements as though this element had not contained any directives. + * + * The compiler does not suspend the entire compilation to wait for templates to be loaded because this + * would result in the whole app "stalling" until all templates are loaded asynchronously - even in the + * case when only one deeply nested directive has `templateUrl`. + * + * Template loading is asynchronous even if the template has been preloaded into the {@link $templateCache} * * You can specify `templateUrl` as a string representing the URL or as a function which takes two * arguments `tElement` and `tAttrs` (described in the `compile` function api below) and returns * a string value representing the url. In either case, the template URL is passed through {@link - * ../api/ng.$sce#methods_getTrustedResourceUrl $sce.getTrustedResourceUrl}. + * $sce#getTrustedResourceUrl $sce.getTrustedResourceUrl}. * * - * #### `replace` - * specify where the template should be inserted. Defaults to `false`. + * #### `replace` ([*DEPRECATED*!], will be removed in next major release - i.e. v2.0) + * specify what the template should replace. Defaults to `false`. * - * * `true` - the template will replace the current element. - * * `false` - the template will replace the contents of the current element. + * * `true` - the template will replace the directive's element. + * * `false` - the template will replace the contents of the directive's element. * + * The replacement process migrates all of the attributes / classes from the old element to the new + * one. See the {@link guide/directive#template-expanding-directive + * Directives Guide} for an example. + * + * There are very few scenarios where element replacement is required for the application function, + * the main one being reusable custom components that are used within SVG contexts + * (because SVG doesn't work with custom elements in the DOM tree). * * #### `transclude` - * compile the content of the element and make it available to the directive. - * Typically used with {@link ../api/ng.directive:ngTransclude - * ngTransclude}. The advantage of transclusion is that the linking function receives a - * transclusion function which is pre-bound to the correct scope. In a typical setup the widget - * creates an `isolate` scope, but the transclusion is not a child, but a sibling of the `isolate` - * scope. This makes it possible for the widget to have private state, and the transclusion to - * be bound to the parent (pre-`isolate`) scope. - * - * * `true` - transclude the content of the directive. - * * `'element'` - transclude the whole element including any directives defined at lower priority. + * Extract the contents of the element where the directive appears and make it available to the directive. + * The contents are compiled and provided to the directive as a **transclusion function**. See the + * {@link $compile#transclusion Transclusion} section below. * * * #### `compile` * - *
    +   * ```js
        *   function compile(tElement, tAttrs, transclude) { ... }
    -   * 
    + * ``` * * The compile function deals with transforming the template DOM. Since most directives do not do - * template transformation, it is not used often. Examples that require compile functions are - * directives that transform template DOM, such as {@link - * ../api/ng.directive:ngRepeat ngRepeat}, or load the contents - * asynchronously, such as {@link ../api/ngRoute.directive:ngView ngView}. The - * compile function takes the following arguments. + * template transformation, it is not used often. The compile function takes the following arguments: * * * `tElement` - template element - The element where the directive has been declared. It is * safe to do template transformation on the element and child elements only. @@ -4972,8 +7631,18 @@ * apply to all cloned DOM nodes within the compile function. Specifically, DOM listener registration * should be done in a linking function rather than in a compile function. *
    + + *
    + * **Note:** The compile function cannot handle directives that recursively use themselves in their + * own templates or compile functions. Compiling these directives results in an infinite loop and + * stack overflow errors. * - *
    + * This can be avoided by manually using $compile in the postLink function to imperatively compile + * a directive's template instead of relying on automatic template compilation via `template` or + * `templateUrl` declaration or manual compilation inside the compile function. + *
    + * + *
    * **Note:** The `transclude` function that is passed to the compile function is deprecated, as it * e.g. does not know about the right outer scope. Please use the transclude function that is passed * to the link function instead. @@ -4992,16 +7661,16 @@ * #### `link` * This property is used only if the `compile` property is not defined. * - *
    +   * ```js
        *   function link(scope, iElement, iAttrs, controller, transcludeFn) { ... }
    -   * 
    + * ``` * * The link function is responsible for registering DOM listeners as well as updating the DOM. It is * executed after the template has been cloned. This is where most of the directive logic will be * put. * - * * `scope` - {@link ../api/ng.$rootScope.Scope Scope} - The scope to be used by the - * directive for registering {@link ../api/ng.$rootScope.Scope#methods_$watch watches}. + * * `scope` - {@link ng.$rootScope.Scope Scope} - The scope to be used by the + * directive for registering {@link ng.$rootScope.Scope#$watch watches}. * * * `iElement` - instance element - The element where the directive is to be used. It is safe to * manipulate the children of the element only in `postLink` function since the children have @@ -5010,15 +7679,23 @@ * * `iAttrs` - instance attributes - Normalized list of attributes declared on this element shared * between all directive linking functions. * - * * `controller` - a controller instance - A controller instance if at least one directive on the - * element defines a controller. The controller is shared among all the directives, which allows - * the directives to use the controllers as a communication channel. + * * `controller` - the directive's required controller instance(s) - Instances are shared + * among all directives, which allows the directives to use the controllers as a communication + * channel. The exact value depends on the directive's `require` property: + * * no controller(s) required: the directive's own controller, or `undefined` if it doesn't have one + * * `string`: the controller instance + * * `array`: array of controller instances + * + * If a required controller cannot be found, and it is optional, the instance is `null`, + * otherwise the {@link error:$compile:ctreq Missing Required Controller} error is thrown. + * + * Note that you can also require the directive's own controller - it will be made available like + * any other controller. * * * `transcludeFn` - A transclude linking function pre-bound to the correct transclusion scope. - * The scope can be overridden by an optional first argument. This is the same as the `$transclude` - * parameter of directive controllers. - * `function([scope], cloneLinkingFn)`. - * + * This is the same as the `$transclude` parameter of directive controllers, + * see {@link ng.$compile#-controller- the controller section for details}. + * `function([scope], cloneLinkingFn, futureParentElement)`. * * #### Pre-linking function * @@ -5027,18 +7704,166 @@ * * #### Post-linking function * - * Executed after the child elements are linked. It is safe to do DOM transformation in the post-linking function. + * Executed after the child elements are linked. + * + * Note that child elements that contain `templateUrl` directives will not have been compiled + * and linked since they are waiting for their template to load asynchronously and their own + * compilation and linking has been suspended until that occurs. + * + * It is safe to do DOM transformation in the post-linking function on elements that are not waiting + * for their async templates to be resolved. + * + * + * ### Transclusion + * + * Transclusion is the process of extracting a collection of DOM elements from one part of the DOM and + * copying them to another part of the DOM, while maintaining their connection to the original AngularJS + * scope from where they were taken. + * + * Transclusion is used (often with {@link ngTransclude}) to insert the + * original contents of a directive's element into a specified place in the template of the directive. + * The benefit of transclusion, over simply moving the DOM elements manually, is that the transcluded + * content has access to the properties on the scope from which it was taken, even if the directive + * has isolated scope. + * See the {@link guide/directive#creating-a-directive-that-wraps-other-elements Directives Guide}. + * + * This makes it possible for the widget to have private state for its template, while the transcluded + * content has access to its originating scope. + * + *
    + * **Note:** When testing an element transclude directive you must not place the directive at the root of the + * DOM fragment that is being compiled. See {@link guide/unit-testing#testing-transclusion-directives + * Testing Transclusion Directives}. + *
    + * + * There are three kinds of transclusion depending upon whether you want to transclude just the contents of the + * directive's element, the entire element or multiple parts of the element contents: + * + * * `true` - transclude the content (i.e. the child nodes) of the directive's element. + * * `'element'` - transclude the whole of the directive's element including any directives on this + * element that defined at a lower priority than this directive. When used, the `template` + * property is ignored. + * * **`{...}` (an object hash):** - map elements of the content onto transclusion "slots" in the template. + * + * **Mult-slot transclusion** is declared by providing an object for the `transclude` property. + * + * This object is a map where the keys are the name of the slot to fill and the value is an element selector + * used to match the HTML to the slot. The element selector should be in normalized form (e.g. `myElement`) + * and will match the standard element variants (e.g. `my-element`, `my:element`, `data-my-element`, etc). + * + * For further information check out the guide on {@link guide/directive#matching-directives Matching Directives} + * + * If the element selector is prefixed with a `?` then that slot is optional. + * + * For example, the transclude object `{ slotA: '?myCustomElement' }` maps `` elements to + * the `slotA` slot, which can be accessed via the `$transclude` function or via the {@link ngTransclude} directive. + * + * Slots that are not marked as optional (`?`) will trigger a compile time error if there are no matching elements + * in the transclude content. If you wish to know if an optional slot was filled with content, then you can call + * `$transclude.isSlotFilled(slotName)` on the transclude function passed to the directive's link function and + * injectable into the directive's controller. + * + * + * #### Transclusion Functions + * + * When a directive requests transclusion, the compiler extracts its contents and provides a **transclusion + * function** to the directive's `link` function and `controller`. This transclusion function is a special + * **linking function** that will return the compiled contents linked to a new transclusion scope. + * + *
    + * If you are just using {@link ngTransclude} then you don't need to worry about this function, since + * ngTransclude will deal with it for us. + *
    + * + * If you want to manually control the insertion and removal of the transcluded content in your directive + * then you must use this transclude function. When you call a transclude function it returns a a jqLite/JQuery + * object that contains the compiled DOM, which is linked to the correct transclusion scope. + * + * When you call a transclusion function you can pass in a **clone attach function**. This function accepts + * two parameters, `function(clone, scope) { ... }`, where the `clone` is a fresh compiled copy of your transcluded + * content and the `scope` is the newly created transclusion scope, which the clone will be linked to. + * + *
    + * **Best Practice**: Always provide a `cloneFn` (clone attach function) when you call a transclude function + * since you then get a fresh clone of the original DOM and also have access to the new transclusion scope. + *
    + * + * It is normal practice to attach your transcluded content (`clone`) to the DOM inside your **clone + * attach function**: + * + * ```js + * var transcludedContent, transclusionScope; + * + * $transclude(function(clone, scope) { + * element.append(clone); + * transcludedContent = clone; + * transclusionScope = scope; + * }); + * ``` + * + * Later, if you want to remove the transcluded content from your DOM then you should also destroy the + * associated transclusion scope: + * + * ```js + * transcludedContent.remove(); + * transclusionScope.$destroy(); + * ``` + * + *
    + * **Best Practice**: if you intend to add and remove transcluded content manually in your directive + * (by calling the transclude function to get the DOM and calling `element.remove()` to remove it), + * then you are also responsible for calling `$destroy` on the transclusion scope. + *
    + * + * The built-in DOM manipulation directives, such as {@link ngIf}, {@link ngSwitch} and {@link ngRepeat} + * automatically destroy their transcluded clones as necessary so you do not need to worry about this if + * you are simply using {@link ngTransclude} to inject the transclusion into your directive. + * + * + * #### Transclusion Scopes + * + * When you call a transclude function it returns a DOM fragment that is pre-bound to a **transclusion + * scope**. This scope is special, in that it is a child of the directive's scope (and so gets destroyed + * when the directive's scope gets destroyed) but it inherits the properties of the scope from which it + * was taken. + * + * For example consider a directive that uses transclusion and isolated scope. The DOM hierarchy might look + * like this: + * + * ```html + *
    + *
    + *
    + *
    + *
    + *
    + * ``` + * + * The `$parent` scope hierarchy will look like this: + * + ``` + - $rootScope + - isolate + - transclusion + ``` + * + * but the scopes will inherit prototypically from different scopes to their `$parent`. + * + ``` + - $rootScope + - transclusion + - isolate + ``` + * * - * * ### Attributes * - * The {@link ../api/ng.$compile.directive.Attributes Attributes} object - passed as a parameter in the + * The {@link ng.$compile.directive.Attributes Attributes} object - passed as a parameter in the * `link()` or `compile()` functions. It has a variety of uses. * - * accessing *Normalized attribute names:* - * Directives like 'ngBind' can be expressed in many ways: 'ng:bind', `data-ng-bind`, or 'x-ng-bind'. - * the attributes object allows for normalized access to - * the attributes. + * * *Accessing normalized attribute names:* Directives like 'ngBind' can be expressed in many ways: + * 'ng:bind', `data-ng-bind`, or 'x-ng-bind'. The attributes object allows for normalized access + * to the attributes. * * * *Directive inter-communication:* All directives share the same instance of the attributes * object which allows the directives to use the attributes object as inter directive @@ -5052,7 +7877,7 @@ * the only way to easily get the actual value because during the linking phase the interpolation * hasn't been evaluated yet and so the value is at this time set to `undefined`. * - *
    +   * ```js
        * function linkingFn(scope, elm, attrs, ctrl) {
      *   // get the attribute value
      *   console.log(attrs.ngModel);
    @@ -5065,19 +7890,19 @@
      *     console.log('ngModel has changed value to ' + value);
      *   });
      * }
    -   * 
    + * ``` * - * Below is an example using `$compileProvider`. + * ## Example * *
    * **Note**: Typically directives are registered with `module.directive`. The example below is * to illustrate how `$compile` works. *
    * - - + + -
    -
    -
    +
    +
    +
    - - + + it('should auto compile', function() { - expect(element('div[compile]').text()).toBe('Hello Angular'); - input('html').enter('{{name}}!'); - expect(element('div[compile]').text()).toBe('Angular!'); + var textarea = $('textarea'); + var output = $('div[compile]'); + // The initial state reads 'Hello Angular'. + expect(output.getText()).toBe('Hello Angular'); + textarea.clear(); + textarea.sendKeys('{{name}}!'); + expect(output.getText()).toBe('Angular!'); }); - - + + * * * @param {string|DOMElement} element Element or HTML string to compile into a template function. - * @param {function(angular.Scope[, cloneAttachFn]} transclude function available to directives. - * @param {number} maxPriority only apply directives lower then given priority (Only effects the + * @param {function(angular.Scope, cloneAttachFn=)} transclude function available to directives - DEPRECATED. + * + *
    + * **Note:** Passing a `transclude` function to the $compile function is deprecated, as it + * e.g. will not use the right outer scope. Please pass the transclude function as a + * `parentBoundTranscludeFn` to the link function instead. + *
    + * + * @param {number} maxPriority only apply directives lower than given priority (Only effects the * root element(s), not their children) - * @returns {function(scope[, cloneAttachFn])} a link function which is used to bind template + * @returns {function(scope, cloneAttachFn=, options=)} a link function which is used to bind template * (a DOM element/tree) to a scope. Where: * * * `scope` - A {@link ng.$rootScope.Scope Scope} to bind to. * * `cloneAttachFn` - If `cloneAttachFn` is provided, then the link function will clone the * `template` and call the `cloneAttachFn` function allowing the caller to attach the * cloned elements to the DOM document at the appropriate place. The `cloneAttachFn` is - * called as:
    `cloneAttachFn(clonedElement, scope)` where: + * called as:
    `cloneAttachFn(clonedElement, scope)` where: * * * `clonedElement` - is a clone of the original `element` passed into the compiler. * * `scope` - is the current scope with which the linking function is working with. * + * * `options` - An optional object hash with linking options. If `options` is provided, then the following + * keys may be used to control linking behavior: + * + * * `parentBoundTranscludeFn` - the transclude function made available to + * directives; if given, it will be passed through to the link functions of + * directives found in `element` during compilation. + * * `transcludeControllers` - an object hash with keys that map controller names + * to a hash with the key `instance`, which maps to the controller instance; + * if given, it will make the controllers available to directives on the compileNode: + * ``` + * { + * parent: { + * instance: parentControllerInstance + * } + * } + * ``` + * * `futureParentElement` - defines the parent to which the `cloneAttachFn` will add + * the cloned elements; only needed for transcludes that are allowed to contain non html + * elements (e.g. SVG elements). See also the directive.controller property. + * * Calling the linking function returns the element of the template. It is either the original * element passed in, or the clone of the element if the `cloneAttachFn` is provided. * @@ -5152,14 +8007,14 @@ * * - If you are not asking the linking function to clone the template, create the DOM element(s) * before you send them to the compiler and keep this reference around. - *
    +   *   ```js
        *     var element = $compile('

    {{total}}

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

    {{total}}

    '), * scope = ....; * @@ -5168,39 +8023,154 @@ * }); * * //now we have reference to the cloned DOM via `clonedElement` - *
    + * ``` * * * For information on how the compiler works, see the * {@link guide/compiler Angular HTML Compiler} section of the Developer Guide. + * + * @knownIssue + * + * ### Double Compilation + * + Double compilation occurs when an already compiled part of the DOM gets + compiled again. This is an undesired effect and can lead to misbehaving directives, performance issues, + and memory leaks. Refer to the Compiler Guide {@link guide/compiler#double-compilation-and-how-to-avoid-it + section on double compilation} for an in-depth explanation and ways to avoid it. + * */ var $compileMinErr = minErr('$compile'); + function UNINITIALIZED_VALUE() {} + var _UNINITIALIZED_VALUE = new UNINITIALIZED_VALUE(); + /** - * @ngdoc service - * @name ng.$compileProvider - * @function + * @ngdoc provider + * @name $compileProvider * * @description */ $CompileProvider.$inject = ['$provide', '$$sanitizeUriProvider']; + /** @this */ function $CompileProvider($provide, $$sanitizeUriProvider) { var hasDirectives = {}, - Suffix = 'Directive', - COMMENT_DIRECTIVE_REGEXP = /^\s*directive\:\s*([\d\w\-_]+)\s+(.*)$/, - CLASS_DIRECTIVE_REGEXP = /(([\d\w\-_]+)(?:\:([^;]+))?;?)/; + Suffix = 'Directive', + COMMENT_DIRECTIVE_REGEXP = /^\s*directive:\s*([\w-]+)\s+(.*)$/, + CLASS_DIRECTIVE_REGEXP = /(([\w-]+)(?::([^;]+))?;?)/, + ALL_OR_NOTHING_ATTRS = makeMap('ngSrc,ngSrcset,src,srcset'), + REQUIRE_PREFIX_REGEXP = /^(?:(\^\^?)?(\?)?(\^\^?)?)?/; // Ref: http://developers.whatwg.org/webappapis.html#event-handler-idl-attributes // The assumption is that future DOM event attribute names will begin with // 'on' and be composed of only English letters. var EVENT_HANDLER_ATTR_REGEXP = /^(on[a-z]+|formaction)$/; + var bindingCache = createMap(); + + function parseIsolateBindings(scope, directiveName, isController) { + var LOCAL_REGEXP = /^\s*([@&<]|=(\*?))(\??)\s*([\w$]*)\s*$/; + + var bindings = createMap(); + + forEach(scope, function(definition, scopeName) { + if (definition in bindingCache) { + bindings[scopeName] = bindingCache[definition]; + return; + } + var match = definition.match(LOCAL_REGEXP); + + if (!match) { + throw $compileMinErr('iscp', + 'Invalid {3} for directive \'{0}\'.' + + ' Definition: {... {1}: \'{2}\' ...}', + directiveName, scopeName, definition, + (isController ? 'controller bindings definition' : + 'isolate scope definition')); + } + + bindings[scopeName] = { + mode: match[1][0], + collection: match[2] === '*', + optional: match[3] === '?', + attrName: match[4] || scopeName + }; + if (match[4]) { + bindingCache[definition] = bindings[scopeName]; + } + }); + + return bindings; + } + + function parseDirectiveBindings(directive, directiveName) { + var bindings = { + isolateScope: null, + bindToController: null + }; + if (isObject(directive.scope)) { + if (directive.bindToController === true) { + bindings.bindToController = parseIsolateBindings(directive.scope, + directiveName, true); + bindings.isolateScope = {}; + } else { + bindings.isolateScope = parseIsolateBindings(directive.scope, + directiveName, false); + } + } + if (isObject(directive.bindToController)) { + bindings.bindToController = + parseIsolateBindings(directive.bindToController, directiveName, true); + } + if (bindings.bindToController && !directive.controller) { + // There is no controller + throw $compileMinErr('noctrl', + 'Cannot bind to controller without directive \'{0}\'s controller.', + directiveName); + } + return bindings; + } + + function assertValidDirectiveName(name) { + var letter = name.charAt(0); + if (!letter || letter !== lowercase(letter)) { + throw $compileMinErr('baddir', 'Directive/Component name \'{0}\' is invalid. The first character must be a lowercase letter', name); + } + if (name !== name.trim()) { + throw $compileMinErr('baddir', + 'Directive/Component name \'{0}\' is invalid. The name should not contain leading or trailing whitespaces', + name); + } + } + + function getDirectiveRequire(directive) { + var require = directive.require || (directive.controller && directive.name); + + if (!isArray(require) && isObject(require)) { + forEach(require, function(value, key) { + var match = value.match(REQUIRE_PREFIX_REGEXP); + var name = value.substring(match[0].length); + if (!name) require[key] = match[0] + key; + }); + } + + return require; + } + + function getDirectiveRestrict(restrict, name) { + if (restrict && !(isString(restrict) && /[EACM]/.test(restrict))) { + throw $compileMinErr('badrestrict', + 'Restrict property \'{0}\' of directive \'{1}\' is invalid', + restrict, + name); + } + + return restrict || 'EA'; + } /** - * @ngdoc function - * @name ng.$compileProvider#directive - * @methodOf ng.$compileProvider - * @function + * @ngdoc method + * @name $compileProvider#directive + * @kind function * * @description * Register a new directive with the compiler. @@ -5208,13 +8178,15 @@ * @param {string|Object} name Name of the directive in camel-case (i.e. ngBind which * will match as ng-bind), or an object map of directives where the keys are the * names and the values are the factories. - * @param {function|Array} directiveFactory An injectable directive factory function. See - * {@link guide/directive} for more info. + * @param {Function|Array} directiveFactory An injectable directive factory function. See the + * {@link guide/directive directive guide} and the {@link $compile compile API} for more info. * @returns {ng.$compileProvider} Self for chaining. */ this.directive = function registerDirective(name, directiveFactory) { + assertArg(name, 'name'); assertNotHasOwnProperty(name, 'directive'); if (isString(name)) { + assertValidDirectiveName(name); assertArg(directiveFactory, 'directiveFactory'); if (!hasDirectives.hasOwnProperty(name)) { hasDirectives[name] = []; @@ -5232,8 +8204,9 @@ directive.priority = directive.priority || 0; directive.index = index; directive.name = directive.name || name; - directive.require = directive.require || (directive.controller && directive.name); - directive.restrict = directive.restrict || 'A'; + directive.require = getDirectiveRequire(directive); + directive.restrict = getDirectiveRestrict(directive.restrict, name); + directive.$$moduleName = directiveFactory.$$moduleName; directives.push(directive); } catch (e) { $exceptionHandler(e); @@ -5249,18 +8222,164 @@ return this; }; + /** + * @ngdoc method + * @name $compileProvider#component + * @module ng + * @param {string|Object} name Name of the component in camelCase (i.e. `myComp` which will match ``), + * or an object map of components where the keys are the names and the values are the component definition objects. + * @param {Object} options Component definition object (a simplified + * {@link ng.$compile#directive-definition-object directive definition object}), + * with the following properties (all optional): + * + * - `controller` – `{(string|function()=}` – controller constructor function that should be + * associated with newly created scope or the name of a {@link ng.$compile#-controller- + * registered controller} if passed as a string. An empty `noop` function by default. + * - `controllerAs` – `{string=}` – identifier name for to reference the controller in the component's scope. + * If present, the controller will be published to scope under the `controllerAs` name. + * If not present, this will default to be `$ctrl`. + * - `template` – `{string=|function()=}` – html template as a string or a function that + * returns an html template as a string which should be used as the contents of this component. + * Empty string by default. + * + * If `template` is a function, then it is {@link auto.$injector#invoke injected} with + * the following locals: + * + * - `$element` - Current element + * - `$attrs` - Current attributes object for the element + * + * - `templateUrl` – `{string=|function()=}` – path or function that returns a path to an html + * template that should be used as the contents of this component. + * + * If `templateUrl` is a function, then it is {@link auto.$injector#invoke injected} with + * the following locals: + * + * - `$element` - Current element + * - `$attrs` - Current attributes object for the element + * + * - `bindings` – `{object=}` – defines bindings between DOM attributes and component properties. + * Component properties are always bound to the component controller and not to the scope. + * See {@link ng.$compile#-bindtocontroller- `bindToController`}. + * - `transclude` – `{boolean=}` – whether {@link $compile#transclusion content transclusion} is enabled. + * Disabled by default. + * - `require` - `{Object=}` - requires the controllers of other directives and binds them to + * this component's controller. The object keys specify the property names under which the required + * controllers (object values) will be bound. See {@link ng.$compile#-require- `require`}. + * - `$...` – additional properties to attach to the directive factory function and the controller + * constructor function. (This is used by the component router to annotate) + * + * @returns {ng.$compileProvider} the compile provider itself, for chaining of function calls. + * @description + * Register a **component definition** with the compiler. This is a shorthand for registering a special + * type of directive, which represents a self-contained UI component in your application. Such components + * are always isolated (i.e. `scope: {}`) and are always restricted to elements (i.e. `restrict: 'E'`). + * + * Component definitions are very simple and do not require as much configuration as defining general + * directives. Component definitions usually consist only of a template and a controller backing it. + * + * In order to make the definition easier, components enforce best practices like use of `controllerAs`, + * `bindToController`. They always have **isolate scope** and are restricted to elements. + * + * Here are a few examples of how you would usually define components: + * + * ```js + * var myMod = angular.module(...); + * myMod.component('myComp', { + * template: '
    My name is {{$ctrl.name}}
    ', + * controller: function() { + * this.name = 'shahar'; + * } + * }); + * + * myMod.component('myComp', { + * template: '
    My name is {{$ctrl.name}}
    ', + * bindings: {name: '@'} + * }); + * + * myMod.component('myComp', { + * templateUrl: 'views/my-comp.html', + * controller: 'MyCtrl', + * controllerAs: 'ctrl', + * bindings: {name: '@'} + * }); + * + * ``` + * For more examples, and an in-depth guide, see the {@link guide/component component guide}. + * + *
    + * See also {@link ng.$compileProvider#directive $compileProvider.directive()}. + */ + this.component = function registerComponent(name, options) { + if (!isString(name)) { + forEach(name, reverseParams(bind(this, registerComponent))); + return this; + } + + var controller = options.controller || function() {}; + + function factory($injector) { + function makeInjectable(fn) { + if (isFunction(fn) || isArray(fn)) { + return /** @this */ function(tElement, tAttrs) { + return $injector.invoke(fn, this, {$element: tElement, $attrs: tAttrs}); + }; + } else { + return fn; + } + } + + var template = (!options.template && !options.templateUrl ? '' : options.template); + var ddo = { + controller: controller, + controllerAs: identifierForController(options.controller) || options.controllerAs || '$ctrl', + template: makeInjectable(template), + templateUrl: makeInjectable(options.templateUrl), + transclude: options.transclude, + scope: {}, + bindToController: options.bindings || {}, + restrict: 'E', + require: options.require + }; + + // Copy annotations (starting with $) over to the DDO + forEach(options, function(val, key) { + if (key.charAt(0) === '$') ddo[key] = val; + }); + + return ddo; + } + + // TODO(pete) remove the following `forEach` before we release 1.6.0 + // The component-router@0.2.0 looks for the annotations on the controller constructor + // Nothing in Angular looks for annotations on the factory function but we can't remove + // it from 1.5.x yet. + + // Copy any annotation properties (starting with $) over to the factory and controller constructor functions + // These could be used by libraries such as the new component router + forEach(options, function(val, key) { + if (key.charAt(0) === '$') { + factory[key] = val; + // Don't try to copy over annotations to named controller + if (isFunction(controller)) controller[key] = val; + } + }); + + factory.$inject = ['$injector']; + + return this.directive(name, factory); + }; + /** - * @ngdoc function - * @name ng.$compileProvider#aHrefSanitizationWhitelist - * @methodOf ng.$compileProvider - * @function + * @ngdoc method + * @name $compileProvider#aHrefSanitizationWhitelist + * @kind function * * @description * Retrieves or overrides the default regular expression that is used for whitelisting of safe * urls during a[href] sanitization. * - * The sanitization is a security measure aimed at prevent XSS attacks via html links. + * The sanitization is a security measure aimed at preventing XSS attacks via html links. * * Any url about to be assigned to a[href] via data-binding is first normalized and turned into * an absolute url. Afterwards, the url is matched against the `aHrefSanitizationWhitelist` @@ -5282,10 +8401,9 @@ /** - * @ngdoc function - * @name ng.$compileProvider#imgSrcSanitizationWhitelist - * @methodOf ng.$compileProvider - * @function + * @ngdoc method + * @name $compileProvider#imgSrcSanitizationWhitelist + * @kind function * * @description * Retrieves or overrides the default regular expression that is used for whitelisting of safe @@ -5311,26 +8429,273 @@ } }; - this.$get = [ - '$injector', '$interpolate', '$exceptionHandler', '$http', '$templateCache', '$parse', - '$controller', '$rootScope', '$document', '$sce', '$animate', '$$sanitizeUri', - function($injector, $interpolate, $exceptionHandler, $http, $templateCache, $parse, - $controller, $rootScope, $document, $sce, $animate, $$sanitizeUri) { + /** + * @ngdoc method + * @name $compileProvider#debugInfoEnabled + * + * @param {boolean=} enabled update the debugInfoEnabled state if provided, otherwise just return the + * current debugInfoEnabled state + * @returns {*} current value if used as getter or itself (chaining) if used as setter + * + * @kind function + * + * @description + * Call this method to enable/disable various debug runtime information in the compiler such as adding + * binding information and a reference to the current scope on to DOM elements. + * If enabled, the compiler will add the following to DOM elements that have been bound to the scope + * * `ng-binding` CSS class + * * `$binding` data property containing an array of the binding expressions + * + * You may want to disable this in production for a significant performance boost. See + * {@link guide/production#disabling-debug-data Disabling Debug Data} for more. + * + * The default value is true. + */ + var debugInfoEnabled = true; + this.debugInfoEnabled = function(enabled) { + if (isDefined(enabled)) { + debugInfoEnabled = enabled; + return this; + } + return debugInfoEnabled; + }; + + /** + * @ngdoc method + * @name $compileProvider#preAssignBindingsEnabled + * + * @param {boolean=} enabled update the preAssignBindingsEnabled state if provided, otherwise just return the + * current preAssignBindingsEnabled state + * @returns {*} current value if used as getter or itself (chaining) if used as setter + * + * @kind function + * + * @description + * Call this method to enable/disable whether directive controllers are assigned bindings before + * calling the controller's constructor. + * If enabled (true), the compiler assigns the value of each of the bindings to the + * properties of the controller object before the constructor of this object is called. + * + * If disabled (false), the compiler calls the constructor first before assigning bindings. + * + * The default value is false. + * + * @deprecated + * sinceVersion="1.6.0" + * removeVersion="1.7.0" + * + * This method and the option to assign the bindings before calling the controller's constructor + * will be removed in v1.7.0. + */ + var preAssignBindingsEnabled = false; + this.preAssignBindingsEnabled = function(enabled) { + if (isDefined(enabled)) { + preAssignBindingsEnabled = enabled; + return this; + } + return preAssignBindingsEnabled; + }; + + /** + * @ngdoc method + * @name $compileProvider#strictComponentBindingsEnabled + * + * @param {boolean=} enabled update the strictComponentBindingsEnabled state if provided, otherwise just return the + * current strictComponentBindingsEnabled state + * @returns {*} current value if used as getter or itself (chaining) if used as setter + * + * @kind function + * + * @description + * Call this method to enable/disable strict component bindings check. If enabled, the compiler will enforce that + * for all bindings of a component that are not set as optional with `?`, an attribute needs to be provided + * on the component's HTML tag. + * + * The default value is false. + */ + var strictComponentBindingsEnabled = false; + this.strictComponentBindingsEnabled = function(enabled) { + if (isDefined(enabled)) { + strictComponentBindingsEnabled = enabled; + return this; + } + return strictComponentBindingsEnabled; + }; + + var TTL = 10; + /** + * @ngdoc method + * @name $compileProvider#onChangesTtl + * @description + * + * Sets the number of times `$onChanges` hooks can trigger new changes before giving up and + * assuming that the model is unstable. + * + * The current default is 10 iterations. + * + * In complex applications it's possible that dependencies between `$onChanges` hooks and bindings will result + * in several iterations of calls to these hooks. However if an application needs more than the default 10 + * iterations to stabilize then you should investigate what is causing the model to continuously change during + * the `$onChanges` hook execution. + * + * Increasing the TTL could have performance implications, so you should not change it without proper justification. + * + * @param {number} limit The number of `$onChanges` hook iterations. + * @returns {number|object} the current limit (or `this` if called as a setter for chaining) + */ + this.onChangesTtl = function(value) { + if (arguments.length) { + TTL = value; + return this; + } + return TTL; + }; + + var commentDirectivesEnabledConfig = true; + /** + * @ngdoc method + * @name $compileProvider#commentDirectivesEnabled + * @description + * + * It indicates to the compiler + * whether or not directives on comments should be compiled. + * Defaults to `true`. + * + * Calling this function with false disables the compilation of directives + * on comments for the whole application. + * This results in a compilation performance gain, + * as the compiler doesn't have to check comments when looking for directives. + * This should however only be used if you are sure that no comment directives are used in + * the application (including any 3rd party directives). + * + * @param {boolean} enabled `false` if the compiler may ignore directives on comments + * @returns {boolean|object} the current value (or `this` if called as a setter for chaining) + */ + this.commentDirectivesEnabled = function(value) { + if (arguments.length) { + commentDirectivesEnabledConfig = value; + return this; + } + return commentDirectivesEnabledConfig; + }; + + + var cssClassDirectivesEnabledConfig = true; + /** + * @ngdoc method + * @name $compileProvider#cssClassDirectivesEnabled + * @description + * + * It indicates to the compiler + * whether or not directives on element classes should be compiled. + * Defaults to `true`. + * + * Calling this function with false disables the compilation of directives + * on element classes for the whole application. + * This results in a compilation performance gain, + * as the compiler doesn't have to check element classes when looking for directives. + * This should however only be used if you are sure that no class directives are used in + * the application (including any 3rd party directives). + * + * @param {boolean} enabled `false` if the compiler may ignore directives on element classes + * @returns {boolean|object} the current value (or `this` if called as a setter for chaining) + */ + this.cssClassDirectivesEnabled = function(value) { + if (arguments.length) { + cssClassDirectivesEnabledConfig = value; + return this; + } + return cssClassDirectivesEnabledConfig; + }; + + this.$get = [ + '$injector', '$interpolate', '$exceptionHandler', '$templateRequest', '$parse', + '$controller', '$rootScope', '$sce', '$animate', '$$sanitizeUri', + function($injector, $interpolate, $exceptionHandler, $templateRequest, $parse, + $controller, $rootScope, $sce, $animate, $$sanitizeUri) { + + var SIMPLE_ATTR_NAME = /^\w/; + var specialAttrHolder = window.document.createElement('div'); + + + var commentDirectivesEnabled = commentDirectivesEnabledConfig; + var cssClassDirectivesEnabled = cssClassDirectivesEnabledConfig; + + + var onChangesTtl = TTL; + // The onChanges hooks should all be run together in a single digest + // When changes occur, the call to trigger their hooks will be added to this queue + var onChangesQueue; + + // This function is called in a $$postDigest to trigger all the onChanges hooks in a single digest + function flushOnChangesQueue() { + try { + if (!(--onChangesTtl)) { + // We have hit the TTL limit so reset everything + onChangesQueue = undefined; + throw $compileMinErr('infchng', '{0} $onChanges() iterations reached. Aborting!\n', TTL); + } + // We must run this hook in an apply since the $$postDigest runs outside apply + $rootScope.$apply(function() { + var errors = []; + for (var i = 0, ii = onChangesQueue.length; i < ii; ++i) { + try { + onChangesQueue[i](); + } catch (e) { + errors.push(e); + } + } + // Reset the queue to trigger a new schedule next time there is a change + onChangesQueue = undefined; + if (errors.length) { + throw errors; + } + }); + } finally { + onChangesTtl++; + } + } + + + function Attributes(element, attributesToCopy) { + if (attributesToCopy) { + var keys = Object.keys(attributesToCopy); + var i, l, key; + + for (i = 0, l = keys.length; i < l; i++) { + key = keys[i]; + this[key] = attributesToCopy[key]; + } + } else { + this.$attr = {}; + } - var Attributes = function(element, attr) { this.$$element = element; - this.$attr = attr || {}; - }; + } Attributes.prototype = { + /** + * @ngdoc method + * @name $compile.directive.Attributes#$normalize + * @kind function + * + * @description + * Converts an attribute name (e.g. dash/colon/underscore-delimited string, optionally prefixed with `x-` or + * `data-`) to its normalized, camelCase form. + * + * Also there is special case for Moz prefix starting with upper case letter. + * + * For further information check out the guide on {@link guide/directive#matching-directives Matching Directives} + * + * @param {string} name Name to normalize + */ $normalize: directiveNormalize, /** - * @ngdoc function - * @name ng.$compile.directive.Attributes#$addClass - * @methodOf ng.$compile.directive.Attributes - * @function + * @ngdoc method + * @name $compile.directive.Attributes#$addClass + * @kind function * * @description * Adds the CSS class value specified by the classVal parameter to the element. If animations @@ -5338,17 +8703,16 @@ * * @param {string} classVal The className value that will be added to the element */ - $addClass : function(classVal) { - if(classVal && classVal.length > 0) { + $addClass: function(classVal) { + if (classVal && classVal.length > 0) { $animate.addClass(this.$$element, classVal); } }, /** - * @ngdoc function - * @name ng.$compile.directive.Attributes#$removeClass - * @methodOf ng.$compile.directive.Attributes - * @function + * @ngdoc method + * @name $compile.directive.Attributes#$removeClass + * @kind function * * @description * Removes the CSS class value specified by the classVal parameter from the element. If @@ -5356,17 +8720,16 @@ * * @param {string} classVal The className value that will be removed from the element */ - $removeClass : function(classVal) { - if(classVal && classVal.length > 0) { + $removeClass: function(classVal) { + if (classVal && classVal.length > 0) { $animate.removeClass(this.$$element, classVal); } }, /** - * @ngdoc function - * @name ng.$compile.directive.Attributes#$updateClass - * @methodOf ng.$compile.directive.Attributes - * @function + * @ngdoc method + * @name $compile.directive.Attributes#$updateClass + * @kind function * * @description * Adds and removes the appropriate CSS class values to the element based on the difference @@ -5375,9 +8738,16 @@ * @param {string} newClasses The current CSS className value * @param {string} oldClasses The former CSS className value */ - $updateClass : function(newClasses, oldClasses) { - this.$removeClass(tokenDifference(oldClasses, newClasses)); - this.$addClass(tokenDifference(newClasses, oldClasses)); + $updateClass: function(newClasses, oldClasses) { + var toAdd = tokenDifference(newClasses, oldClasses); + if (toAdd && toAdd.length) { + $animate.addClass(this.$$element, toAdd); + } + + var toRemove = tokenDifference(oldClasses, newClasses); + if (toRemove && toRemove.length) { + $animate.removeClass(this.$$element, toRemove); + } }, /** @@ -5394,13 +8764,18 @@ //is set through this function since it may cause $updateClass to //become unstable. - var booleanKey = getBooleanAttrName(this.$$element[0], key), - normalizedVal, - nodeName; + var node = this.$$element[0], + booleanKey = getBooleanAttrName(node, key), + aliasedKey = getAliasedAttrName(key), + observer = key, + nodeName; if (booleanKey) { this.$$element.prop(key, value); attrName = booleanKey; + } else if (aliasedKey) { + this[aliasedKey] = value; + observer = aliasedKey; } this[key] = value; @@ -5417,37 +8792,76 @@ nodeName = nodeName_(this.$$element); - // sanitize a[href] and img[src] values - if ((nodeName === 'A' && key === 'href') || - (nodeName === 'IMG' && key === 'src')) { + if ((nodeName === 'a' && (key === 'href' || key === 'xlinkHref')) || + (nodeName === 'img' && key === 'src')) { + // sanitize a[href] and img[src] values this[key] = value = $$sanitizeUri(value, key === 'src'); + } else if (nodeName === 'img' && key === 'srcset' && isDefined(value)) { + // sanitize img[srcset] values + var result = ''; + + // first check if there are spaces because it's not the same pattern + var trimmedSrcset = trim(value); + // ( 999x ,| 999w ,| ,|, ) + var srcPattern = /(\s+\d+x\s*,|\s+\d+w\s*,|\s+,|,\s+)/; + var pattern = /\s/.test(trimmedSrcset) ? srcPattern : /(,)/; + + // split srcset into tuple of uri and descriptor except for the last item + var rawUris = trimmedSrcset.split(pattern); + + // for each tuples + var nbrUrisWith2parts = Math.floor(rawUris.length / 2); + for (var i = 0; i < nbrUrisWith2parts; i++) { + var innerIdx = i * 2; + // sanitize the uri + result += $$sanitizeUri(trim(rawUris[innerIdx]), true); + // add the descriptor + result += (' ' + trim(rawUris[innerIdx + 1])); + } + + // split the last item into uri and descriptor + var lastTuple = trim(rawUris[i * 2]).split(/\s/); + + // sanitize the last uri + result += $$sanitizeUri(trim(lastTuple[0]), true); + + // and add the last descriptor if any + if (lastTuple.length === 2) { + result += (' ' + trim(lastTuple[1])); + } + this[key] = value = result; } if (writeAttr !== false) { - if (value === null || value === undefined) { + if (value === null || isUndefined(value)) { this.$$element.removeAttr(attrName); } else { - this.$$element.attr(attrName, value); + if (SIMPLE_ATTR_NAME.test(attrName)) { + this.$$element.attr(attrName, value); + } else { + setSpecialAttr(this.$$element[0], attrName, value); + } } } // fire observers var $$observers = this.$$observers; - $$observers && forEach($$observers[key], function(fn) { - try { - fn(value); - } catch (e) { - $exceptionHandler(e); - } - }); + if ($$observers) { + forEach($$observers[observer], function(fn) { + try { + fn(value); + } catch (e) { + $exceptionHandler(e); + } + }); + } }, /** - * @ngdoc function - * @name ng.$compile.directive.Attributes#$observe - * @methodOf ng.$compile.directive.Attributes - * @function + * @ngdoc method + * @name $compile.directive.Attributes#$observe + * @kind function * * @description * Observes an interpolated attribute. @@ -5459,34 +8873,95 @@ * @param {string} key Normalized key. (ie ngAttribute) . * @param {function(interpolatedValue)} fn Function that will be called whenever the interpolated value of the attribute changes. - * See the {@link guide/directive#Attributes Directives} guide for more info. - * @returns {function()} the `fn` parameter. + * See the {@link guide/interpolation#how-text-and-attribute-bindings-work Interpolation + * guide} for more info. + * @returns {function()} Returns a deregistration function for this observer. */ $observe: function(key, fn) { var attrs = this, - $$observers = (attrs.$$observers || (attrs.$$observers = {})), - listeners = ($$observers[key] || ($$observers[key] = [])); + $$observers = (attrs.$$observers || (attrs.$$observers = createMap())), + listeners = ($$observers[key] || ($$observers[key] = [])); listeners.push(fn); $rootScope.$evalAsync(function() { - if (!listeners.$$inter) { + if (!listeners.$$inter && attrs.hasOwnProperty(key) && !isUndefined(attrs[key])) { // no one registered attribute interpolation function, so lets call it manually fn(attrs[key]); } }); - return fn; + + return function() { + arrayRemove(listeners, fn); + }; } }; + function setSpecialAttr(element, attrName, value) { + // Attributes names that do not start with letters (such as `(click)`) cannot be set using `setAttribute` + // so we have to jump through some hoops to get such an attribute + // https://github.com/angular/angular.js/pull/13318 + specialAttrHolder.innerHTML = ''; + var attributes = specialAttrHolder.firstChild.attributes; + var attribute = attributes[0]; + // We have to remove the attribute from its container element before we can add it to the destination element + attributes.removeNamedItem(attribute.name); + attribute.value = value; + element.attributes.setNamedItem(attribute); + } + + function safeAddClass($element, className) { + try { + $element.addClass(className); + } catch (e) { + // ignore, since it means that we are trying to set class on + // SVG element, where class name is read-only. + } + } + + var startSymbol = $interpolate.startSymbol(), - endSymbol = $interpolate.endSymbol(), - denormalizeTemplate = (startSymbol == '{{' || endSymbol == '}}') - ? identity - : function denormalizeTemplate(template) { + endSymbol = $interpolate.endSymbol(), + denormalizeTemplate = (startSymbol === '{{' && endSymbol === '}}') + ? identity + : function denormalizeTemplate(template) { return template.replace(/\{\{/g, startSymbol).replace(/}}/g, endSymbol); }, - NG_ATTR_BINDING = /^ngAttr[A-Z]/; + NG_ATTR_BINDING = /^ngAttr[A-Z]/; + var MULTI_ELEMENT_DIR_RE = /^(.+)Start$/; + compile.$$addBindingInfo = debugInfoEnabled ? function $$addBindingInfo($element, binding) { + var bindings = $element.data('$binding') || []; + + if (isArray(binding)) { + bindings = bindings.concat(binding); + } else { + bindings.push(binding); + } + + $element.data('$binding', bindings); + } : noop; + + compile.$$addBindingClass = debugInfoEnabled ? function $$addBindingClass($element) { + safeAddClass($element, 'ng-binding'); + } : noop; + + compile.$$addScopeInfo = debugInfoEnabled ? function $$addScopeInfo($element, scope, isolated, noTemplate) { + var dataName = isolated ? (noTemplate ? '$isolateScopeNoTemplate' : '$isolateScope') : '$scope'; + $element.data(dataName, scope); + } : noop; + + compile.$$addScopeClass = debugInfoEnabled ? function $$addScopeClass($element, isolated) { + safeAddClass($element, isolated ? 'ng-isolate-scope' : 'ng-scope'); + } : noop; + + compile.$$createComment = function(directiveName, comment) { + var content = ''; + if (debugInfoEnabled) { + content = ' ' + (directiveName || '') + ': '; + if (comment) content += comment + ' '; + } + return window.document.createComment(content); + }; return compile; @@ -5499,50 +8974,84 @@ // modify it. $compileNodes = jqLite($compileNodes); } - // We can not compile top level text elements since text nodes can be merged and we will - // not be able to attach scope data to them, so we will wrap them in - forEach($compileNodes, function(node, index){ - if (node.nodeType == 3 /* text node */ && node.nodeValue.match(/\S+/) /* non-empty */ ) { - $compileNodes[index] = node = jqLite(node).wrap('').parent()[0]; - } - }); var compositeLinkFn = - compileNodes($compileNodes, transcludeFn, $compileNodes, - maxPriority, ignoreDirective, previousCompileContext); - safeAddClass($compileNodes, 'ng-scope'); - return function publicLinkFn(scope, cloneConnectFn, transcludeControllers){ + compileNodes($compileNodes, transcludeFn, $compileNodes, + maxPriority, ignoreDirective, previousCompileContext); + compile.$$addScopeClass($compileNodes); + var namespace = null; + return function publicLinkFn(scope, cloneConnectFn, options) { + if (!$compileNodes) { + throw $compileMinErr('multilink', 'This element has already been linked.'); + } assertArg(scope, 'scope'); - // important!!: we must call our jqLite.clone() since the jQuery one is trying to be smart - // and sometimes changes the structure of the DOM. - var $linkNode = cloneConnectFn - ? JQLitePrototype.clone.call($compileNodes) // IMPORTANT!!! - : $compileNodes; - forEach(transcludeControllers, function(instance, name) { - $linkNode.data('$' + name + 'Controller', instance); - }); + if (previousCompileContext && previousCompileContext.needsNewScope) { + // A parent directive did a replace and a directive on this element asked + // for transclusion, which caused us to lose a layer of element on which + // we could hold the new transclusion scope, so we will create it manually + // here. + scope = scope.$parent.$new(); + } - // Attach scope only to non-text nodes. - for(var i = 0, ii = $linkNode.length; i').append($compileNodes).html()) + ); + } else if (cloneConnectFn) { + // important!!: we must call our jqLite.clone() since the jQuery one is trying to be smart + // and sometimes changes the structure of the DOM. + $linkNode = JQLitePrototype.clone.call($compileNodes); + } else { + $linkNode = $compileNodes; + } + + if (transcludeControllers) { + for (var controllerName in transcludeControllers) { + $linkNode.data('$' + controllerName + 'Controller', transcludeControllers[controllerName].instance); } } + compile.$$addScopeInfo($linkNode, scope); + if (cloneConnectFn) cloneConnectFn($linkNode, scope); - if (compositeLinkFn) compositeLinkFn(scope, $linkNode, $linkNode); + if (compositeLinkFn) compositeLinkFn(scope, $linkNode, $linkNode, parentBoundTranscludeFn); + + if (!cloneConnectFn) { + $compileNodes = compositeLinkFn = null; + } return $linkNode; }; } - function safeAddClass($element, className) { - try { - $element.addClass(className); - } catch(e) { - // ignore, since it means that we are trying to set class on - // SVG element, where class name is read-only. + function detectNamespaceForChildElements(parentElement) { + // TODO: Make this detect MathML as well... + var node = parentElement && parentElement[0]; + if (!node) { + return 'html'; + } else { + return nodeName_(node) !== 'foreignobject' && toString.call(node).match(/SVG/) ? 'svg' : 'html'; } } @@ -5553,44 +9062,61 @@ * function, which is the a linking function for the node. * * @param {NodeList} nodeList an array of nodes or NodeList to compile - * @param {function(angular.Scope[, cloneAttachFn]} transcludeFn A linking function, where the + * @param {function(angular.Scope, cloneAttachFn=)} transcludeFn A linking function, where the * scope argument is auto-generated to the new child of the transcluded parent scope. * @param {DOMElement=} $rootElement If the nodeList is the root of the compilation tree then * the rootElement must be set the jqLite collection of the compile root. This is * needed so that the jqLite collection items can be replaced with widgets. * @param {number=} maxPriority Max directive priority. - * @returns {?function} A composite linking function of all of the matched directives or null. + * @returns {Function} A composite linking function of all of the matched directives or null. */ function compileNodes(nodeList, transcludeFn, $rootElement, maxPriority, ignoreDirective, previousCompileContext) { var linkFns = [], - attrs, directives, nodeLinkFn, childNodes, childLinkFn, linkFnFound; + // `nodeList` can be either an element's `.childNodes` (live NodeList) + // or a jqLite/jQuery collection or an array + notLiveList = isArray(nodeList) || (nodeList instanceof jqLite), + attrs, directives, nodeLinkFn, childNodes, childLinkFn, linkFnFound, nodeLinkFnFound; + for (var i = 0; i < nodeList.length; i++) { attrs = new Attributes(); - // we must always refer to nodeList[i] since the nodes can be replaced underneath us. + // Support: IE 11 only + // Workaround for #11781 and #14924 + if (msie === 11) { + mergeConsecutiveTextNodes(nodeList, i, notLiveList); + } + + // We must always refer to `nodeList[i]` hereafter, + // since the nodes can be replaced underneath us. directives = collectDirectives(nodeList[i], [], attrs, i === 0 ? maxPriority : undefined, - ignoreDirective); + ignoreDirective); nodeLinkFn = (directives.length) - ? applyDirectivesToNode(directives, nodeList[i], attrs, transcludeFn, $rootElement, + ? applyDirectivesToNode(directives, nodeList[i], attrs, transcludeFn, $rootElement, null, [], [], previousCompileContext) - : null; + : null; if (nodeLinkFn && nodeLinkFn.scope) { - safeAddClass(jqLite(nodeList[i]), 'ng-scope'); + compile.$$addScopeClass(attrs.$$element); } childLinkFn = (nodeLinkFn && nodeLinkFn.terminal || - !(childNodes = nodeList[i].childNodes) || - !childNodes.length) - ? null - : compileNodes(childNodes, - nodeLinkFn ? nodeLinkFn.transclude : transcludeFn); + !(childNodes = nodeList[i].childNodes) || + !childNodes.length) + ? null + : compileNodes(childNodes, + nodeLinkFn ? ( + (nodeLinkFn.transcludeOnThisElement || !nodeLinkFn.templateOnThisElement) + && nodeLinkFn.transclude) : transcludeFn); + + if (nodeLinkFn || childLinkFn) { + linkFns.push(i, nodeLinkFn, childLinkFn); + linkFnFound = true; + nodeLinkFnFound = nodeLinkFnFound || nodeLinkFn; + } - linkFns.push(nodeLinkFn, childLinkFn); - linkFnFound = linkFnFound || nodeLinkFn || childLinkFn; //use the previous context only for the first element in the virtual group previousCompileContext = null; } @@ -5598,60 +9124,115 @@ // return a linking function if we have found anything, null otherwise return linkFnFound ? compositeLinkFn : null; - function compositeLinkFn(scope, nodeList, $rootElement, boundTranscludeFn) { - var nodeLinkFn, childLinkFn, node, $node, childScope, childTranscludeFn, i, ii, n; + function compositeLinkFn(scope, nodeList, $rootElement, parentBoundTranscludeFn) { + var nodeLinkFn, childLinkFn, node, childScope, i, ii, idx, childBoundTranscludeFn; + var stableNodeList; - // copy nodeList so that linking doesn't break due to live list updates. - var nodeListLength = nodeList.length, - stableNodeList = new Array(nodeListLength); - for (i = 0; i < nodeListLength; i++) { - stableNodeList[i] = nodeList[i]; + + if (nodeLinkFnFound) { + // copy nodeList so that if a nodeLinkFn removes or adds an element at this DOM level our + // offsets don't get screwed up + var nodeListLength = nodeList.length; + stableNodeList = new Array(nodeListLength); + + // create a sparse array by only copying the elements which have a linkFn + for (i = 0; i < linkFns.length; i += 3) { + idx = linkFns[i]; + stableNodeList[idx] = nodeList[idx]; + } + } else { + stableNodeList = nodeList; } - for(i = 0, n = 0, ii = linkFns.length; i < ii; n++) { - node = stableNodeList[n]; + for (i = 0, ii = linkFns.length; i < ii;) { + node = stableNodeList[linkFns[i++]]; nodeLinkFn = linkFns[i++]; childLinkFn = linkFns[i++]; - $node = jqLite(node); if (nodeLinkFn) { if (nodeLinkFn.scope) { childScope = scope.$new(); - $node.data('$scope', childScope); + compile.$$addScopeInfo(jqLite(node), childScope); } else { childScope = scope; } - childTranscludeFn = nodeLinkFn.transclude; - if (childTranscludeFn || (!boundTranscludeFn && transcludeFn)) { - nodeLinkFn(childLinkFn, childScope, node, $rootElement, - createBoundTranscludeFn(scope, childTranscludeFn || transcludeFn) - ); + + if (nodeLinkFn.transcludeOnThisElement) { + childBoundTranscludeFn = createBoundTranscludeFn( + scope, nodeLinkFn.transclude, parentBoundTranscludeFn); + + } else if (!nodeLinkFn.templateOnThisElement && parentBoundTranscludeFn) { + childBoundTranscludeFn = parentBoundTranscludeFn; + + } else if (!parentBoundTranscludeFn && transcludeFn) { + childBoundTranscludeFn = createBoundTranscludeFn(scope, transcludeFn); + } else { - nodeLinkFn(childLinkFn, childScope, node, $rootElement, boundTranscludeFn); + childBoundTranscludeFn = null; } + + nodeLinkFn(childLinkFn, childScope, node, $rootElement, childBoundTranscludeFn); + } else if (childLinkFn) { - childLinkFn(scope, node.childNodes, undefined, boundTranscludeFn); + childLinkFn(scope, node.childNodes, undefined, parentBoundTranscludeFn); } } } } - function createBoundTranscludeFn(scope, transcludeFn) { - return function boundTranscludeFn(transcludedScope, cloneFn, controllers) { - var scopeCreated = false; + function mergeConsecutiveTextNodes(nodeList, idx, notLiveList) { + var node = nodeList[idx]; + var parent = node.parentNode; + var sibling; + + if (node.nodeType !== NODE_TYPE_TEXT) { + return; + } + + while (true) { + sibling = parent ? node.nextSibling : nodeList[idx + 1]; + if (!sibling || sibling.nodeType !== NODE_TYPE_TEXT) { + break; + } + + node.nodeValue = node.nodeValue + sibling.nodeValue; + + if (sibling.parentNode) { + sibling.parentNode.removeChild(sibling); + } + if (notLiveList && sibling === nodeList[idx + 1]) { + nodeList.splice(idx + 1, 1); + } + } + } + + function createBoundTranscludeFn(scope, transcludeFn, previousBoundTranscludeFn) { + function boundTranscludeFn(transcludedScope, cloneFn, controllers, futureParentElement, containingScope) { if (!transcludedScope) { - transcludedScope = scope.$new(); + transcludedScope = scope.$new(false, containingScope); transcludedScope.$$transcluded = true; - scopeCreated = true; } - var clone = transcludeFn(transcludedScope, cloneFn, controllers); - if (scopeCreated) { - clone.on('$destroy', bind(transcludedScope, transcludedScope.$destroy)); + return transcludeFn(transcludedScope, cloneFn, { + parentBoundTranscludeFn: previousBoundTranscludeFn, + transcludeControllers: controllers, + futureParentElement: futureParentElement + }); + } + + // We need to attach the transclusion slots onto the `boundTranscludeFn` + // so that they are available inside the `controllersBoundTransclude` function + var boundSlots = boundTranscludeFn.$$slots = createMap(); + for (var slotName in transcludeFn.$$slots) { + if (transcludeFn.$$slots[slotName]) { + boundSlots[slotName] = createBoundTranscludeFn(scope, transcludeFn.$$slots[slotName], previousBoundTranscludeFn); + } else { + boundSlots[slotName] = null; } - return clone; - }; + } + + return boundTranscludeFn; } /** @@ -5666,54 +9247,75 @@ */ function collectDirectives(node, directives, attrs, maxPriority, ignoreDirective) { var nodeType = node.nodeType, - attrsMap = attrs.$attr, - match, - className; + attrsMap = attrs.$attr, + match, + nodeName, + className; + + switch (nodeType) { + case NODE_TYPE_ELEMENT: /* Element */ + + nodeName = nodeName_(node); - switch(nodeType) { - case 1: /* Element */ // use the node name: addDirective(directives, - directiveNormalize(nodeName_(node).toLowerCase()), 'E', maxPriority, ignoreDirective); + directiveNormalize(nodeName), 'E', maxPriority, ignoreDirective); // iterate over the attributes - for (var attr, name, nName, ngAttrName, value, nAttrs = node.attributes, - j = 0, jj = nAttrs && nAttrs.length; j < jj; j++) { + for (var attr, name, nName, ngAttrName, value, isNgAttr, nAttrs = node.attributes, + j = 0, jj = nAttrs && nAttrs.length; j < jj; j++) { var attrStartName = false; var attrEndName = false; attr = nAttrs[j]; - if (!msie || msie >= 8 || attr.specified) { - name = attr.name; - // support ngAttr attribute binding - ngAttrName = directiveNormalize(name); - if (NG_ATTR_BINDING.test(ngAttrName)) { - name = snake_case(ngAttrName.substr(6), '-'); - } + name = attr.name; + value = attr.value; - var directiveNName = ngAttrName.replace(/(Start|End)$/, ''); - if (ngAttrName === directiveNName + 'Start') { - attrStartName = name; - attrEndName = name.substr(0, name.length - 5) + 'end'; - name = name.substr(0, name.length - 6); - } + // support ngAttr attribute binding + ngAttrName = directiveNormalize(name); + isNgAttr = NG_ATTR_BINDING.test(ngAttrName); + if (isNgAttr) { + name = name.replace(PREFIX_REGEXP, '') + .substr(8).replace(/_(.)/g, function(match, letter) { + return letter.toUpperCase(); + }); + } - nName = directiveNormalize(name.toLowerCase()); - attrsMap[nName] = name; - attrs[nName] = value = trim(attr.value); + var multiElementMatch = ngAttrName.match(MULTI_ELEMENT_DIR_RE); + if (multiElementMatch && directiveIsMultiElement(multiElementMatch[1])) { + attrStartName = name; + attrEndName = name.substr(0, name.length - 5) + 'end'; + name = name.substr(0, name.length - 6); + } + + nName = directiveNormalize(name.toLowerCase()); + attrsMap[nName] = name; + if (isNgAttr || !attrs.hasOwnProperty(nName)) { + attrs[nName] = value; if (getBooleanAttrName(node, nName)) { attrs[nName] = true; // presence means true } - addAttrInterpolateDirective(node, directives, value, nName); - addDirective(directives, nName, 'A', maxPriority, ignoreDirective, attrStartName, - attrEndName); } + addAttrInterpolateDirective(node, directives, value, nName, isNgAttr); + addDirective(directives, nName, 'A', maxPriority, ignoreDirective, attrStartName, + attrEndName); + } + + if (nodeName === 'input' && node.getAttribute('type') === 'hidden') { + // Hidden input elements can have strange behaviour when navigating back to the page + // This tells the browser not to try to cache and reinstate previous values + node.setAttribute('autocomplete', 'off'); } // use class as directive + if (!cssClassDirectivesEnabled) break; className = node.className; + if (isObject(className)) { + // Maybe SVGAnimatedString + className = className.animVal; + } if (isString(className) && className !== '') { - while (match = CLASS_DIRECTIVE_REGEXP.exec(className)) { + while ((match = CLASS_DIRECTIVE_REGEXP.exec(className))) { nName = directiveNormalize(match[2]); if (addDirective(directives, nName, 'C', maxPriority, ignoreDirective)) { attrs[nName] = trim(match[3]); @@ -5722,23 +9324,12 @@ } } break; - case 3: /* Text Node */ + case NODE_TYPE_TEXT: /* Text Node */ addTextInterpolateDirective(directives, node.nodeValue); break; - case 8: /* Comment */ - try { - match = COMMENT_DIRECTIVE_REGEXP.exec(node.nodeValue); - if (match) { - nName = directiveNormalize(match[1]); - if (addDirective(directives, nName, 'M', maxPriority, ignoreDirective)) { - attrs[nName] = trim(match[2]); - } - } - } catch (e) { - // turns out that under some circumstances IE9 throws errors when one attempts to read - // comment's node value. - // Just ignore it and continue. (Can't seem to reproduce in test case.) - } + case NODE_TYPE_COMMENT: /* Comment */ + if (!commentDirectivesEnabled) break; + collectCommentDirectives(node, directives, attrs, maxPriority, ignoreDirective); break; } @@ -5746,8 +9337,26 @@ return directives; } + function collectCommentDirectives(node, directives, attrs, maxPriority, ignoreDirective) { + // function created because of performance, try/catch disables + // the optimization of the whole function #14848 + try { + var match = COMMENT_DIRECTIVE_REGEXP.exec(node.nodeValue); + if (match) { + var nName = directiveNormalize(match[1]); + if (addDirective(directives, nName, 'M', maxPriority, ignoreDirective)) { + attrs[nName] = trim(match[2]); + } + } + } catch (e) { + // turns out that under some circumstances IE9 throws errors when one attempts to read + // comment's node value. + // Just ignore it and continue. (Can't seem to reproduce in test case.) + } + } + /** - * Given a node with an directive-start it collects all of the siblings until it finds + * Given a node with a directive-start it collects all of the siblings until it finds * directive-end. * @param node * @param attrStart @@ -5758,14 +9367,13 @@ var nodes = []; var depth = 0; if (attrStart && node.hasAttribute && node.hasAttribute(attrStart)) { - var startNode = node; do { if (!node) { throw $compileMinErr('uterdir', - "Unterminated attribute, found '{0}' but no matching '{1}' found.", - attrStart, attrEnd); + 'Unterminated attribute, found \'{0}\' but no matching \'{1}\' found.', + attrStart, attrEnd); } - if (node.nodeType == 1 /** Element **/) { + if (node.nodeType === NODE_TYPE_ELEMENT) { if (node.hasAttribute(attrStart)) depth++; if (node.hasAttribute(attrEnd)) depth--; } @@ -5788,12 +9396,41 @@ * @returns {Function} */ function groupElementsLinkFnWrapper(linkFn, attrStart, attrEnd) { - return function(scope, element, attrs, controllers, transcludeFn) { + return function groupedElementsLink(scope, element, attrs, controllers, transcludeFn) { element = groupScan(element[0], attrStart, attrEnd); return linkFn(scope, element, attrs, controllers, transcludeFn); }; } + /** + * A function generator that is used to support both eager and lazy compilation + * linking function. + * @param eager + * @param $compileNodes + * @param transcludeFn + * @param maxPriority + * @param ignoreDirective + * @param previousCompileContext + * @returns {Function} + */ + function compilationGenerator(eager, $compileNodes, transcludeFn, maxPriority, ignoreDirective, previousCompileContext) { + var compiled; + + if (eager) { + return compile($compileNodes, transcludeFn, maxPriority, ignoreDirective, previousCompileContext); + } + return /** @this */ function lazyCompilation() { + if (!compiled) { + compiled = compile($compileNodes, transcludeFn, maxPriority, ignoreDirective, previousCompileContext); + + // Null out all of these references in order to make them eligible for garbage collection + // since this is a potentially long lived closure + $compileNodes = transcludeFn = previousCompileContext = null; + } + return compiled.apply(this, arguments); + }; + } + /** * Once the directives have been collected, their compile functions are executed. This method * is responsible for inlining directive templates as well as terminating the application @@ -5803,7 +9440,7 @@ * this needs to be pre-sorted by priority order. * @param {Node} compileNode The raw DOM node to apply the compile functions to * @param {Object} templateAttrs The shared attribute function - * @param {function(angular.Scope[, cloneAttachFn]} transcludeFn A linking function, where the + * @param {function(angular.Scope, cloneAttachFn=)} transcludeFn A linking function, where the * scope argument is auto-generated to the new * child of the transcluded parent scope. * @param {JQLite} jqCollection If we are working on the root of the compile tree then this @@ -5815,7 +9452,7 @@ * @param {Array.} postLinkFns * @param {Object} previousCompileContext Context used for previous compilation of the current * node - * @returns linkFn + * @returns {Function} linkFn */ function applyDirectivesToNode(directives, compileNode, templateAttrs, transcludeFn, jqCollection, originalReplaceDirective, preLinkFns, postLinkFns, @@ -5823,24 +9460,27 @@ previousCompileContext = previousCompileContext || {}; var terminalPriority = -Number.MAX_VALUE, - newScopeDirective, - controllerDirectives = previousCompileContext.controllerDirectives, - newIsolateScopeDirective = previousCompileContext.newIsolateScopeDirective, - templateDirective = previousCompileContext.templateDirective, - nonTlbTranscludeDirective = previousCompileContext.nonTlbTranscludeDirective, - hasTranscludeDirective = false, - hasElementTranscludeDirective = false, - $compileNode = templateAttrs.$$element = jqLite(compileNode), - directive, - directiveName, - $template, - replaceDirective = originalReplaceDirective, - childTranscludeFn = transcludeFn, - linkFn, - directiveValue; + newScopeDirective = previousCompileContext.newScopeDirective, + controllerDirectives = previousCompileContext.controllerDirectives, + newIsolateScopeDirective = previousCompileContext.newIsolateScopeDirective, + templateDirective = previousCompileContext.templateDirective, + nonTlbTranscludeDirective = previousCompileContext.nonTlbTranscludeDirective, + hasTranscludeDirective = false, + hasTemplate = false, + hasElementTranscludeDirective = previousCompileContext.hasElementTranscludeDirective, + $compileNode = templateAttrs.$$element = jqLite(compileNode), + directive, + directiveName, + $template, + replaceDirective = originalReplaceDirective, + childTranscludeFn = transcludeFn, + linkFn, + didScanForMultipleTransclusion = false, + mightHaveMultipleTransclusionError = false, + directiveValue; // executes all directives on the current element - for(var i = 0, ii = directives.length; i < ii; i++) { + for (var i = 0, ii = directives.length; i < ii; i++) { directive = directives[i]; var attrStart = directive.$$start; var attrEnd = directive.$$end; @@ -5855,90 +9495,195 @@ break; // prevent further processing of directives } - if (directiveValue = directive.scope) { - newScopeDirective = newScopeDirective || directive; + directiveValue = directive.scope; + + if (directiveValue) { // skip the check for directives with async templates, we'll check the derived sync // directive when the template arrives if (!directive.templateUrl) { - assertNoDuplicate('new/isolated scope', newIsolateScopeDirective, directive, - $compileNode); if (isObject(directiveValue)) { + // This directive is trying to add an isolated scope. + // Check that there is no scope of any kind already + assertNoDuplicate('new/isolated scope', newIsolateScopeDirective || newScopeDirective, + directive, $compileNode); newIsolateScopeDirective = directive; + } else { + // This directive is trying to add a child scope. + // Check that there is no isolated scope already + assertNoDuplicate('new/isolated scope', newIsolateScopeDirective, directive, + $compileNode); } } + + newScopeDirective = newScopeDirective || directive; } directiveName = directive.name; + // If we encounter a condition that can result in transclusion on the directive, + // then scan ahead in the remaining directives for others that may cause a multiple + // transclusion error to be thrown during the compilation process. If a matching directive + // is found, then we know that when we encounter a transcluded directive, we need to eagerly + // compile the `transclude` function rather than doing it lazily in order to throw + // exceptions at the correct time + if (!didScanForMultipleTransclusion && ((directive.replace && (directive.templateUrl || directive.template)) + || (directive.transclude && !directive.$$tlb))) { + var candidateDirective; + + for (var scanningIndex = i + 1; (candidateDirective = directives[scanningIndex++]);) { + if ((candidateDirective.transclude && !candidateDirective.$$tlb) + || (candidateDirective.replace && (candidateDirective.templateUrl || candidateDirective.template))) { + mightHaveMultipleTransclusionError = true; + break; + } + } + + didScanForMultipleTransclusion = true; + } + if (!directive.templateUrl && directive.controller) { - directiveValue = directive.controller; - controllerDirectives = controllerDirectives || {}; - assertNoDuplicate("'" + directiveName + "' controller", - controllerDirectives[directiveName], directive, $compileNode); + controllerDirectives = controllerDirectives || createMap(); + assertNoDuplicate('\'' + directiveName + '\' controller', + controllerDirectives[directiveName], directive, $compileNode); controllerDirectives[directiveName] = directive; } - if (directiveValue = directive.transclude) { + directiveValue = directive.transclude; + + if (directiveValue) { hasTranscludeDirective = true; // Special case ngIf and ngRepeat so that we don't complain about duplicate transclusion. - // This option should only be used by directives that know how to how to safely handle element transclusion, + // This option should only be used by directives that know how to safely handle element transclusion, // where the transcluded nodes are added or replaced after linking. if (!directive.$$tlb) { assertNoDuplicate('transclusion', nonTlbTranscludeDirective, directive, $compileNode); nonTlbTranscludeDirective = directive; } - if (directiveValue == 'element') { + if (directiveValue === 'element') { hasElementTranscludeDirective = true; terminalPriority = directive.priority; - $template = groupScan(compileNode, attrStart, attrEnd); + $template = $compileNode; $compileNode = templateAttrs.$$element = - jqLite(document.createComment(' ' + directiveName + ': ' + - templateAttrs[directiveName] + ' ')); + jqLite(compile.$$createComment(directiveName, templateAttrs[directiveName])); compileNode = $compileNode[0]; - replaceWith(jqCollection, jqLite(sliceArgs($template)), compileNode); + replaceWith(jqCollection, sliceArgs($template), compileNode); - childTranscludeFn = compile($template, transcludeFn, terminalPriority, - replaceDirective && replaceDirective.name, { - // Don't pass in: - // - controllerDirectives - otherwise we'll create duplicates controllers - // - newIsolateScopeDirective or templateDirective - combining templates with - // element transclusion doesn't make sense. - // - // We need only nonTlbTranscludeDirective so that we prevent putting transclusion - // on the same element more than once. - nonTlbTranscludeDirective: nonTlbTranscludeDirective - }); + // Support: Chrome < 50 + // https://github.com/angular/angular.js/issues/14041 + + // In the versions of V8 prior to Chrome 50, the document fragment that is created + // in the `replaceWith` function is improperly garbage collected despite still + // being referenced by the `parentNode` property of all of the child nodes. By adding + // a reference to the fragment via a different property, we can avoid that incorrect + // behavior. + // TODO: remove this line after Chrome 50 has been released + $template[0].$$parentNode = $template[0].parentNode; + + childTranscludeFn = compilationGenerator(mightHaveMultipleTransclusionError, $template, transcludeFn, terminalPriority, + replaceDirective && replaceDirective.name, { + // Don't pass in: + // - controllerDirectives - otherwise we'll create duplicates controllers + // - newIsolateScopeDirective or templateDirective - combining templates with + // element transclusion doesn't make sense. + // + // We need only nonTlbTranscludeDirective so that we prevent putting transclusion + // on the same element more than once. + nonTlbTranscludeDirective: nonTlbTranscludeDirective + }); } else { - $template = jqLite(jqLiteClone(compileNode)).contents(); + + var slots = createMap(); + + if (!isObject(directiveValue)) { + $template = jqLite(jqLiteClone(compileNode)).contents(); + } else { + + // We have transclusion slots, + // collect them up, compile them and store their transclusion functions + $template = []; + + var slotMap = createMap(); + var filledSlots = createMap(); + + // Parse the element selectors + forEach(directiveValue, function(elementSelector, slotName) { + // If an element selector starts with a ? then it is optional + var optional = (elementSelector.charAt(0) === '?'); + elementSelector = optional ? elementSelector.substring(1) : elementSelector; + + slotMap[elementSelector] = slotName; + + // We explicitly assign `null` since this implies that a slot was defined but not filled. + // Later when calling boundTransclusion functions with a slot name we only error if the + // slot is `undefined` + slots[slotName] = null; + + // filledSlots contains `true` for all slots that are either optional or have been + // filled. This is used to check that we have not missed any required slots + filledSlots[slotName] = optional; + }); + + // Add the matching elements into their slot + forEach($compileNode.contents(), function(node) { + var slotName = slotMap[directiveNormalize(nodeName_(node))]; + if (slotName) { + filledSlots[slotName] = true; + slots[slotName] = slots[slotName] || []; + slots[slotName].push(node); + } else { + $template.push(node); + } + }); + + // Check for required slots that were not filled + forEach(filledSlots, function(filled, slotName) { + if (!filled) { + throw $compileMinErr('reqslot', 'Required transclusion slot `{0}` was not filled.', slotName); + } + }); + + for (var slotName in slots) { + if (slots[slotName]) { + // Only define a transclusion function if the slot was filled + slots[slotName] = compilationGenerator(mightHaveMultipleTransclusionError, slots[slotName], transcludeFn); + } + } + } + $compileNode.empty(); // clear contents - childTranscludeFn = compile($template, transcludeFn); + childTranscludeFn = compilationGenerator(mightHaveMultipleTransclusionError, $template, transcludeFn, undefined, + undefined, { needsNewScope: directive.$$isolateScope || directive.$$newScope}); + childTranscludeFn.$$slots = slots; } } if (directive.template) { + hasTemplate = true; assertNoDuplicate('template', templateDirective, directive, $compileNode); templateDirective = directive; directiveValue = (isFunction(directive.template)) - ? directive.template($compileNode, templateAttrs) - : directive.template; + ? directive.template($compileNode, templateAttrs) + : directive.template; directiveValue = denormalizeTemplate(directiveValue); if (directive.replace) { replaceDirective = directive; - $template = jqLite('
    ' + - trim(directiveValue) + - '
    ').contents(); + if (jqLiteIsTextNode(directiveValue)) { + $template = []; + } else { + $template = removeComments(wrapTemplate(directive.templateNamespace, trim(directiveValue))); + } compileNode = $template[0]; - if ($template.length != 1 || compileNode.nodeType !== 1) { + if ($template.length !== 1 || compileNode.nodeType !== NODE_TYPE_ELEMENT) { throw $compileMinErr('tplrt', - "Template for directive '{0}' must have exactly one root element. {1}", - directiveName, ''); + 'Template for directive \'{0}\' must have exactly one root element. {1}', + directiveName, ''); } replaceWith(jqCollection, $compileNode, compileNode); @@ -5953,8 +9698,11 @@ var templateDirectives = collectDirectives(compileNode, [], newTemplateAttrs); var unprocessedDirectives = directives.splice(i + 1, directives.length - (i + 1)); - if (newIsolateScopeDirective) { - markDirectivesAsIsolate(templateDirectives); + if (newIsolateScopeDirective || newScopeDirective) { + // The original directive caused the current element to be replaced but this element + // also needs to have a new scope, so we need to tell the template directives + // that they would need to get their scope from further up, if they require transclusion + markDirectiveScope(templateDirectives, newIsolateScopeDirective, newScopeDirective); } directives = directives.concat(templateDirectives).concat(unprocessedDirectives); mergeTemplateAttributes(templateAttrs, newTemplateAttrs); @@ -5966,6 +9714,7 @@ } if (directive.templateUrl) { + hasTemplate = true; assertNoDuplicate('template', templateDirective, directive, $compileNode); templateDirective = directive; @@ -5973,21 +9722,24 @@ replaceDirective = directive; } + // eslint-disable-next-line no-func-assign nodeLinkFn = compileTemplateUrl(directives.splice(i, directives.length - i), $compileNode, - templateAttrs, jqCollection, childTranscludeFn, preLinkFns, postLinkFns, { - controllerDirectives: controllerDirectives, - newIsolateScopeDirective: newIsolateScopeDirective, - templateDirective: templateDirective, - nonTlbTranscludeDirective: nonTlbTranscludeDirective - }); + templateAttrs, jqCollection, hasTranscludeDirective && childTranscludeFn, preLinkFns, postLinkFns, { + controllerDirectives: controllerDirectives, + newScopeDirective: (newScopeDirective !== directive) && newScopeDirective, + newIsolateScopeDirective: newIsolateScopeDirective, + templateDirective: templateDirective, + nonTlbTranscludeDirective: nonTlbTranscludeDirective + }); ii = directives.length; } else if (directive.compile) { try { linkFn = directive.compile($compileNode, templateAttrs, childTranscludeFn); + var context = directive.$$originalDirective || directive; if (isFunction(linkFn)) { - addLinkFns(null, linkFn, attrStart, attrEnd); + addLinkFns(null, bind(context, linkFn), attrStart, attrEnd); } else if (linkFn) { - addLinkFns(linkFn.pre, linkFn.post, attrStart, attrEnd); + addLinkFns(bind(context, linkFn.pre), bind(context, linkFn.post), attrStart, attrEnd); } } catch (e) { $exceptionHandler(e, startingTag($compileNode)); @@ -6002,7 +9754,11 @@ } nodeLinkFn.scope = newScopeDirective && newScopeDirective.scope === true; - nodeLinkFn.transclude = hasTranscludeDirective && childTranscludeFn; + nodeLinkFn.transcludeOnThisElement = hasTranscludeDirective; + nodeLinkFn.templateOnThisElement = hasTemplate; + nodeLinkFn.transclude = childTranscludeFn; + + previousCompileContext.hasElementTranscludeDirective = hasElementTranscludeDirective; // might be normal or delayed nodeLinkFn depending on if templateUrl is present return nodeLinkFn; @@ -6013,6 +9769,7 @@ if (pre) { if (attrStart) pre = groupElementsLinkFnWrapper(pre, attrStart, attrEnd); pre.require = directive.require; + pre.directiveName = directiveName; if (newIsolateScopeDirective === directive || directive.$$isolateScope) { pre = cloneAndAnnotateFn(pre, {isolateScope: true}); } @@ -6021,6 +9778,7 @@ if (post) { if (attrStart) post = groupElementsLinkFnWrapper(post, attrStart, attrEnd); post.require = directive.require; + post.directiveName = directiveName; if (newIsolateScopeDirective === directive || directive.$$isolateScope) { post = cloneAndAnnotateFn(post, {isolateScope: true}); } @@ -6028,180 +9786,135 @@ } } - - function getControllers(require, $element, elementControllers) { - var value, retrievalMethod = 'data', optional = false; - if (isString(require)) { - while((value = require.charAt(0)) == '^' || value == '?') { - require = require.substr(1); - if (value == '^') { - retrievalMethod = 'inheritedData'; - } - optional = optional || value == '?'; - } - value = null; - - if (elementControllers && retrievalMethod === 'data') { - value = elementControllers[require]; - } - value = value || $element[retrievalMethod]('$' + require + 'Controller'); - - if (!value && !optional) { - throw $compileMinErr('ctreq', - "Controller '{0}', required by directive '{1}', can't be found!", - require, directiveName); - } - return value; - } else if (isArray(require)) { - value = []; - forEach(require, function(require) { - value.push(getControllers(require, $element, elementControllers)); - }); - } - return value; - } - - function nodeLinkFn(childLinkFn, scope, linkNode, $rootElement, boundTranscludeFn) { - var attrs, $element, i, ii, linkFn, controller, isolateScope, elementControllers = {}, transcludeFn; + var i, ii, linkFn, isolateScope, controllerScope, elementControllers, transcludeFn, $element, + attrs, scopeBindingInfo; if (compileNode === linkNode) { attrs = templateAttrs; + $element = templateAttrs.$$element; } else { - attrs = shallowCopy(templateAttrs, new Attributes(jqLite(linkNode), templateAttrs.$attr)); + $element = jqLite(linkNode); + attrs = new Attributes($element, templateAttrs); + } + + controllerScope = scope; + if (newIsolateScopeDirective) { + isolateScope = scope.$new(true); + } else if (newScopeDirective) { + controllerScope = scope.$parent; + } + + if (boundTranscludeFn) { + // track `boundTranscludeFn` so it can be unwrapped if `transcludeFn` + // is later passed as `parentBoundTranscludeFn` to `publicLinkFn` + transcludeFn = controllersBoundTransclude; + transcludeFn.$$boundTransclude = boundTranscludeFn; + // expose the slots on the `$transclude` function + transcludeFn.isSlotFilled = function(slotName) { + return !!boundTranscludeFn.$$slots[slotName]; + }; + } + + if (controllerDirectives) { + elementControllers = setupControllers($element, attrs, transcludeFn, controllerDirectives, isolateScope, scope, newIsolateScopeDirective); } - $element = attrs.$$element; if (newIsolateScopeDirective) { - var LOCAL_REGEXP = /^\s*([@=&])(\??)\s*(\w*)\s*$/; - var $linkNode = jqLite(linkNode); - - isolateScope = scope.$new(true); - - if (templateDirective && (templateDirective === newIsolateScopeDirective.$$originalDirective)) { - $linkNode.data('$isolateScope', isolateScope) ; - } else { - $linkNode.data('$isolateScopeNoTemplate', isolateScope); + // Initialize isolate scope bindings for new isolate scope directive. + compile.$$addScopeInfo($element, isolateScope, true, !(templateDirective && (templateDirective === newIsolateScopeDirective || + templateDirective === newIsolateScopeDirective.$$originalDirective))); + compile.$$addScopeClass($element, true); + isolateScope.$$isolateBindings = + newIsolateScopeDirective.$$isolateBindings; + scopeBindingInfo = initializeDirectiveBindings(scope, attrs, isolateScope, + isolateScope.$$isolateBindings, + newIsolateScopeDirective); + if (scopeBindingInfo.removeWatches) { + isolateScope.$on('$destroy', scopeBindingInfo.removeWatches); } - - - - safeAddClass($linkNode, 'ng-isolate-scope'); - - forEach(newIsolateScopeDirective.scope, function(definition, scopeName) { - var match = definition.match(LOCAL_REGEXP) || [], - attrName = match[3] || scopeName, - optional = (match[2] == '?'), - mode = match[1], // @, =, or & - lastValue, - parentGet, parentSet, compare; - - isolateScope.$$isolateBindings[scopeName] = mode + attrName; - - switch (mode) { - - case '@': - attrs.$observe(attrName, function(value) { - isolateScope[scopeName] = value; - }); - attrs.$$observers[attrName].$$scope = scope; - if( attrs[attrName] ) { - // If the attribute has been provided then we trigger an interpolation to ensure - // the value is there for use in the link fn - isolateScope[scopeName] = $interpolate(attrs[attrName])(scope); - } - break; - - case '=': - if (optional && !attrs[attrName]) { - return; - } - parentGet = $parse(attrs[attrName]); - if (parentGet.literal) { - compare = equals; - } else { - compare = function(a,b) { return a === b; }; - } - parentSet = parentGet.assign || function() { - // reset the change, or we will throw this exception on every $digest - lastValue = isolateScope[scopeName] = parentGet(scope); - throw $compileMinErr('nonassign', - "Expression '{0}' used with directive '{1}' is non-assignable!", - attrs[attrName], newIsolateScopeDirective.name); - }; - lastValue = isolateScope[scopeName] = parentGet(scope); - isolateScope.$watch(function parentValueWatch() { - var parentValue = parentGet(scope); - if (!compare(parentValue, isolateScope[scopeName])) { - // we are out of sync and need to copy - if (!compare(parentValue, lastValue)) { - // parent changed and it has precedence - isolateScope[scopeName] = parentValue; - } else { - // if the parent can be assigned then do so - parentSet(scope, parentValue = isolateScope[scopeName]); - } - } - return lastValue = parentValue; - }, null, parentGet.literal); - break; - - case '&': - parentGet = $parse(attrs[attrName]); - isolateScope[scopeName] = function(locals) { - return parentGet(scope, locals); - }; - break; - - default: - throw $compileMinErr('iscp', - "Invalid isolate scope definition for directive '{0}'." + - " Definition: {... {1}: '{2}' ...}", - newIsolateScopeDirective.name, scopeName, definition); - } - }); } - transcludeFn = boundTranscludeFn && controllersBoundTransclude; - if (controllerDirectives) { - forEach(controllerDirectives, function(directive) { - var locals = { - $scope: directive === newIsolateScopeDirective || directive.$$isolateScope ? isolateScope : scope, - $element: $element, - $attrs: attrs, - $transclude: transcludeFn - }, controllerInstance; - controller = directive.controller; - if (controller == '@') { - controller = attrs[directive.name]; + // Initialize bindToController bindings + for (var name in elementControllers) { + var controllerDirective = controllerDirectives[name]; + var controller = elementControllers[name]; + var bindings = controllerDirective.$$bindings.bindToController; + + if (preAssignBindingsEnabled) { + if (bindings) { + controller.bindingInfo = + initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective); + } else { + controller.bindingInfo = {}; } - controllerInstance = $controller(controller, locals); - // For directives with element transclusion the element is a comment, - // but jQuery .data doesn't support attaching data to comment nodes as it's hard to - // clean up (http://bugs.jquery.com/ticket/8335). - // Instead, we save the controllers for the element in a local hash and attach to .data - // later, once we have the actual element. - elementControllers[directive.name] = controllerInstance; - if (!hasElementTranscludeDirective) { - $element.data('$' + directive.name + 'Controller', controllerInstance); + var controllerResult = controller(); + if (controllerResult !== controller.instance) { + // If the controller constructor has a return value, overwrite the instance + // from setupControllers + controller.instance = controllerResult; + $element.data('$' + controllerDirective.name + 'Controller', controllerResult); + if (controller.bindingInfo.removeWatches) { + controller.bindingInfo.removeWatches(); + } + controller.bindingInfo = + initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective); } - - if (directive.controllerAs) { - locals.$scope[directive.controllerAs] = controllerInstance; - } - }); + } else { + controller.instance = controller(); + $element.data('$' + controllerDirective.name + 'Controller', controller.instance); + controller.bindingInfo = + initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective); + } } + // Bind the required controllers to the controller, if `require` is an object and `bindToController` is truthy + forEach(controllerDirectives, function(controllerDirective, name) { + var require = controllerDirective.require; + if (controllerDirective.bindToController && !isArray(require) && isObject(require)) { + extend(elementControllers[name].instance, getControllers(name, require, $element, elementControllers)); + } + }); + + // Handle the init and destroy lifecycle hooks on all controllers that have them + forEach(elementControllers, function(controller) { + var controllerInstance = controller.instance; + if (isFunction(controllerInstance.$onChanges)) { + try { + controllerInstance.$onChanges(controller.bindingInfo.initialChanges); + } catch (e) { + $exceptionHandler(e); + } + } + if (isFunction(controllerInstance.$onInit)) { + try { + controllerInstance.$onInit(); + } catch (e) { + $exceptionHandler(e); + } + } + if (isFunction(controllerInstance.$doCheck)) { + controllerScope.$watch(function() { controllerInstance.$doCheck(); }); + controllerInstance.$doCheck(); + } + if (isFunction(controllerInstance.$onDestroy)) { + controllerScope.$on('$destroy', function callOnDestroyHook() { + controllerInstance.$onDestroy(); + }); + } + }); + // PRELINKING - for(i = 0, ii = preLinkFns.length; i < ii; i++) { - try { - linkFn = preLinkFns[i]; - linkFn(linkFn.isolateScope ? isolateScope : scope, $element, attrs, - linkFn.require && getControllers(linkFn.require, $element, elementControllers), transcludeFn); - } catch (e) { - $exceptionHandler(e, startingTag($element)); - } + for (i = 0, ii = preLinkFns.length; i < ii; i++) { + linkFn = preLinkFns[i]; + invokeLinkFn(linkFn, + linkFn.isolateScope ? isolateScope : scope, + $element, + attrs, + linkFn.require && getControllers(linkFn.directiveName, linkFn.require, $element, elementControllers), + transcludeFn + ); } // RECURSION @@ -6211,25 +9924,38 @@ if (newIsolateScopeDirective && (newIsolateScopeDirective.template || newIsolateScopeDirective.templateUrl === null)) { scopeToChild = isolateScope; } - childLinkFn && childLinkFn(scopeToChild, linkNode.childNodes, undefined, boundTranscludeFn); - - // POSTLINKING - for(i = postLinkFns.length - 1; i >= 0; i--) { - try { - linkFn = postLinkFns[i]; - linkFn(linkFn.isolateScope ? isolateScope : scope, $element, attrs, - linkFn.require && getControllers(linkFn.require, $element, elementControllers), transcludeFn); - } catch (e) { - $exceptionHandler(e, startingTag($element)); - } + if (childLinkFn) { + childLinkFn(scopeToChild, linkNode.childNodes, undefined, boundTranscludeFn); } - // This is the function that is injected as `$transclude`. - function controllersBoundTransclude(scope, cloneAttachFn) { - var transcludeControllers; + // POSTLINKING + for (i = postLinkFns.length - 1; i >= 0; i--) { + linkFn = postLinkFns[i]; + invokeLinkFn(linkFn, + linkFn.isolateScope ? isolateScope : scope, + $element, + attrs, + linkFn.require && getControllers(linkFn.directiveName, linkFn.require, $element, elementControllers), + transcludeFn + ); + } - // no scope passed - if (arguments.length < 2) { + // Trigger $postLink lifecycle hooks + forEach(elementControllers, function(controller) { + var controllerInstance = controller.instance; + if (isFunction(controllerInstance.$postLink)) { + controllerInstance.$postLink(); + } + }); + + // This is the function that is injected as `$transclude`. + // Note: all arguments are optional! + function controllersBoundTransclude(scope, cloneAttachFn, futureParentElement, slotName) { + var transcludeControllers; + // No scope passed in: + if (!isScope(scope)) { + slotName = futureParentElement; + futureParentElement = cloneAttachFn; cloneAttachFn = scope; scope = undefined; } @@ -6237,16 +9963,111 @@ if (hasElementTranscludeDirective) { transcludeControllers = elementControllers; } - - return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers); + if (!futureParentElement) { + futureParentElement = hasElementTranscludeDirective ? $element.parent() : $element; + } + if (slotName) { + // slotTranscludeFn can be one of three things: + // * a transclude function - a filled slot + // * `null` - an optional slot that was not filled + // * `undefined` - a slot that was not declared (i.e. invalid) + var slotTranscludeFn = boundTranscludeFn.$$slots[slotName]; + if (slotTranscludeFn) { + return slotTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild); + } else if (isUndefined(slotTranscludeFn)) { + throw $compileMinErr('noslot', + 'No parent directive that requires a transclusion with slot name "{0}". ' + + 'Element: {1}', + slotName, startingTag($element)); + } + } else { + return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild); + } } } } - function markDirectivesAsIsolate(directives) { - // mark all directives as needing isolate scope. + function getControllers(directiveName, require, $element, elementControllers) { + var value; + + if (isString(require)) { + var match = require.match(REQUIRE_PREFIX_REGEXP); + var name = require.substring(match[0].length); + var inheritType = match[1] || match[3]; + var optional = match[2] === '?'; + + //If only parents then start at the parent element + if (inheritType === '^^') { + $element = $element.parent(); + //Otherwise attempt getting the controller from elementControllers in case + //the element is transcluded (and has no data) and to avoid .data if possible + } else { + value = elementControllers && elementControllers[name]; + value = value && value.instance; + } + + if (!value) { + var dataName = '$' + name + 'Controller'; + value = inheritType ? $element.inheritedData(dataName) : $element.data(dataName); + } + + if (!value && !optional) { + throw $compileMinErr('ctreq', + 'Controller \'{0}\', required by directive \'{1}\', can\'t be found!', + name, directiveName); + } + } else if (isArray(require)) { + value = []; + for (var i = 0, ii = require.length; i < ii; i++) { + value[i] = getControllers(directiveName, require[i], $element, elementControllers); + } + } else if (isObject(require)) { + value = {}; + forEach(require, function(controller, property) { + value[property] = getControllers(directiveName, controller, $element, elementControllers); + }); + } + + return value || null; + } + + function setupControllers($element, attrs, transcludeFn, controllerDirectives, isolateScope, scope, newIsolateScopeDirective) { + var elementControllers = createMap(); + for (var controllerKey in controllerDirectives) { + var directive = controllerDirectives[controllerKey]; + var locals = { + $scope: directive === newIsolateScopeDirective || directive.$$isolateScope ? isolateScope : scope, + $element: $element, + $attrs: attrs, + $transclude: transcludeFn + }; + + var controller = directive.controller; + if (controller === '@') { + controller = attrs[directive.name]; + } + + var controllerInstance = $controller(controller, locals, true, directive.controllerAs); + + // For directives with element transclusion the element is a comment. + // In this case .data will not attach any data. + // Instead, we save the controllers for the element in a local hash and attach to .data + // later, once we have the actual element. + elementControllers[directive.name] = controllerInstance; + $element.data('$' + directive.name + 'Controller', controllerInstance.instance); + } + return elementControllers; + } + + // Depending upon the context in which a directive finds itself it might need to have a new isolated + // or child scope created. For instance: + // * if the directive has been pulled into a template because another directive with a higher priority + // asked for element transclusion + // * if the directive itself asks for transclusion but it is at the root of a template and the original + // element was replaced. See https://github.com/angular/angular.js/issues/12936 + function markDirectiveScope(directives, isolateScope, newScope) { for (var j = 0, jj = directives.length; j < jj; j++) { - directives[j] = inherit(directives[j], {$$isolateScope: true}); + directives[j] = inherit(directives[j], {$$isolateScope: isolateScope, $$newScope: newScope}); } } @@ -6262,32 +10083,58 @@ * * `A': attribute * * `C`: class * * `M`: comment - * @returns true if directive was added. + * @returns {boolean} true if directive was added. */ function addDirective(tDirectives, name, location, maxPriority, ignoreDirective, startAttrName, endAttrName) { if (name === ignoreDirective) return null; var match = null; if (hasDirectives.hasOwnProperty(name)) { - for(var directive, directives = $injector.get(name + Suffix), - i = 0, ii = directives.length; i directive.priority) && - directive.restrict.indexOf(location) != -1) { - if (startAttrName) { - directive = inherit(directive, {$$start: startAttrName, $$end: endAttrName}); - } - tDirectives.push(directive); - match = directive; + for (var directive, directives = $injector.get(name + Suffix), + i = 0, ii = directives.length; i < ii; i++) { + directive = directives[i]; + if ((isUndefined(maxPriority) || maxPriority > directive.priority) && + directive.restrict.indexOf(location) !== -1) { + if (startAttrName) { + directive = inherit(directive, {$$start: startAttrName, $$end: endAttrName}); } - } catch(e) { $exceptionHandler(e); } + if (!directive.$$bindings) { + var bindings = directive.$$bindings = + parseDirectiveBindings(directive, directive.name); + if (isObject(bindings.isolateScope)) { + directive.$$isolateBindings = bindings.isolateScope; + } + } + tDirectives.push(directive); + match = directive; + } } } return match; } + /** + * looks up the directive and returns true if it is a multi-element directive, + * and therefore requires DOM nodes between -start and -end markers to be grouped + * together. + * + * @param {string} name name of the directive to look up. + * @returns true if directive was registered as multi-element. + */ + function directiveIsMultiElement(name) { + if (hasDirectives.hasOwnProperty(name)) { + for (var directive, directives = $injector.get(name + Suffix), + i = 0, ii = directives.length; i < ii; i++) { + directive = directives[i]; + if (directive.multiElement) { + return true; + } + } + } + return false; + } + /** * When the element is replaced with HTML template then the new attributes * on the template need to be merged with the existing attributes in the DOM. @@ -6298,14 +10145,17 @@ */ function mergeTemplateAttributes(dst, src) { var srcAttr = src.$attr, - dstAttr = dst.$attr, - $element = dst.$$element; + dstAttr = dst.$attr; // reapply the old attributes to the new element forEach(dst, function(value, key) { - if (key.charAt(0) != '$') { - if (src[key]) { - value += (key === 'style' ? ';' : ' ') + src[key]; + if (key.charAt(0) !== '$') { + if (src[key] && src[key] !== value) { + if (value.length) { + value += (key === 'style' ? ';' : ' ') + src[key]; + } else { + value = src[key]; + } } dst.$set(key, value, true, srcAttr[key]); } @@ -6313,18 +10163,16 @@ // copy the new attributes on the old attrs object forEach(src, function(value, key) { - if (key == 'class') { - safeAddClass($element, value); - dst['class'] = (dst['class'] ? dst['class'] + ' ' : '') + value; - } else if (key == 'style') { - $element.attr('style', $element.attr('style') + ';' + value); - dst['style'] = (dst['style'] ? dst['style'] + ';' : '') + value; - // `dst` will never contain hasOwnProperty as DOM parser won't let it. - // You will get an "InvalidCharacterError: DOM Exception 5" error if you - // have an attribute like "has-own-property" or "data-has-own-property", etc. - } else if (key.charAt(0) != '$' && !dst.hasOwnProperty(key)) { + // Check if we already set this attribute in the loop above. + // `dst` will never contain hasOwnProperty as DOM parser won't let it. + // You will get an "InvalidCharacterError: DOM Exception 5" error if you + // have an attribute like "has-own-property" or "data-has-own-property", etc. + if (!dst.hasOwnProperty(key) && key.charAt(0) !== '$') { dst[key] = value; - dstAttr[key] = srcAttr[key]; + + if (key !== 'class' && key !== 'style') { + dstAttr[key] = srcAttr[key]; + } } }); } @@ -6333,97 +10181,118 @@ function compileTemplateUrl(directives, $compileNode, tAttrs, $rootElement, childTranscludeFn, preLinkFns, postLinkFns, previousCompileContext) { var linkQueue = [], - afterTemplateNodeLinkFn, - afterTemplateChildLinkFn, - beforeTemplateCompileNode = $compileNode[0], - origAsyncDirective = directives.shift(), - // The fact that we have to copy and patch the directive seems wrong! - derivedSyncDirective = extend({}, origAsyncDirective, { - templateUrl: null, transclude: null, replace: null, $$originalDirective: origAsyncDirective - }), - templateUrl = (isFunction(origAsyncDirective.templateUrl)) - ? origAsyncDirective.templateUrl($compileNode, tAttrs) - : origAsyncDirective.templateUrl; + afterTemplateNodeLinkFn, + afterTemplateChildLinkFn, + beforeTemplateCompileNode = $compileNode[0], + origAsyncDirective = directives.shift(), + derivedSyncDirective = inherit(origAsyncDirective, { + templateUrl: null, transclude: null, replace: null, $$originalDirective: origAsyncDirective + }), + templateUrl = (isFunction(origAsyncDirective.templateUrl)) + ? origAsyncDirective.templateUrl($compileNode, tAttrs) + : origAsyncDirective.templateUrl, + templateNamespace = origAsyncDirective.templateNamespace; $compileNode.empty(); - $http.get($sce.getTrustedResourceUrl(templateUrl), {cache: $templateCache}). - success(function(content) { - var compileNode, tempTemplateAttrs, $template, childBoundTranscludeFn; + $templateRequest(templateUrl) + .then(function(content) { + var compileNode, tempTemplateAttrs, $template, childBoundTranscludeFn; - content = denormalizeTemplate(content); + content = denormalizeTemplate(content); - if (origAsyncDirective.replace) { - $template = jqLite('
    ' + trim(content) + '
    ').contents(); - compileNode = $template[0]; - - if ($template.length != 1 || compileNode.nodeType !== 1) { - throw $compileMinErr('tplrt', - "Template for directive '{0}' must have exactly one root element. {1}", - origAsyncDirective.name, templateUrl); - } - - tempTemplateAttrs = {$attr: {}}; - replaceWith($rootElement, $compileNode, compileNode); - var templateDirectives = collectDirectives(compileNode, [], tempTemplateAttrs); - - if (isObject(origAsyncDirective.scope)) { - markDirectivesAsIsolate(templateDirectives); - } - directives = templateDirectives.concat(directives); - mergeTemplateAttributes(tAttrs, tempTemplateAttrs); + if (origAsyncDirective.replace) { + if (jqLiteIsTextNode(content)) { + $template = []; } else { - compileNode = beforeTemplateCompileNode; - $compileNode.html(content); + $template = removeComments(wrapTemplate(templateNamespace, trim(content))); + } + compileNode = $template[0]; + + if ($template.length !== 1 || compileNode.nodeType !== NODE_TYPE_ELEMENT) { + throw $compileMinErr('tplrt', + 'Template for directive \'{0}\' must have exactly one root element. {1}', + origAsyncDirective.name, templateUrl); } - directives.unshift(derivedSyncDirective); + tempTemplateAttrs = {$attr: {}}; + replaceWith($rootElement, $compileNode, compileNode); + var templateDirectives = collectDirectives(compileNode, [], tempTemplateAttrs); - afterTemplateNodeLinkFn = applyDirectivesToNode(directives, compileNode, tAttrs, - childTranscludeFn, $compileNode, origAsyncDirective, preLinkFns, postLinkFns, - previousCompileContext); - forEach($rootElement, function(node, i) { - if (node == compileNode) { - $rootElement[i] = $compileNode[0]; - } - }); - afterTemplateChildLinkFn = compileNodes($compileNode[0].childNodes, childTranscludeFn); + if (isObject(origAsyncDirective.scope)) { + // the original directive that caused the template to be loaded async required + // an isolate scope + markDirectiveScope(templateDirectives, true); + } + directives = templateDirectives.concat(directives); + mergeTemplateAttributes(tAttrs, tempTemplateAttrs); + } else { + compileNode = beforeTemplateCompileNode; + $compileNode.html(content); + } + directives.unshift(derivedSyncDirective); - while(linkQueue.length) { - var scope = linkQueue.shift(), - beforeTemplateLinkNode = linkQueue.shift(), - linkRootElement = linkQueue.shift(), - boundTranscludeFn = linkQueue.shift(), - linkNode = $compileNode[0]; + afterTemplateNodeLinkFn = applyDirectivesToNode(directives, compileNode, tAttrs, + childTranscludeFn, $compileNode, origAsyncDirective, preLinkFns, postLinkFns, + previousCompileContext); + forEach($rootElement, function(node, i) { + if (node === compileNode) { + $rootElement[i] = $compileNode[0]; + } + }); + afterTemplateChildLinkFn = compileNodes($compileNode[0].childNodes, childTranscludeFn); - if (beforeTemplateLinkNode !== beforeTemplateCompileNode) { + while (linkQueue.length) { + var scope = linkQueue.shift(), + beforeTemplateLinkNode = linkQueue.shift(), + linkRootElement = linkQueue.shift(), + boundTranscludeFn = linkQueue.shift(), + linkNode = $compileNode[0]; + + if (scope.$$destroyed) continue; + + if (beforeTemplateLinkNode !== beforeTemplateCompileNode) { + var oldClasses = beforeTemplateLinkNode.className; + + if (!(previousCompileContext.hasElementTranscludeDirective && + origAsyncDirective.replace)) { // it was cloned therefore we have to clone as well. linkNode = jqLiteClone(compileNode); - replaceWith(linkRootElement, jqLite(beforeTemplateLinkNode), linkNode); } - if (afterTemplateNodeLinkFn.transclude) { - childBoundTranscludeFn = createBoundTranscludeFn(scope, afterTemplateNodeLinkFn.transclude); - } else { - childBoundTranscludeFn = boundTranscludeFn; - } - afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, linkNode, $rootElement, - childBoundTranscludeFn); + replaceWith(linkRootElement, jqLite(beforeTemplateLinkNode), linkNode); + + // Copy in CSS classes from original node + safeAddClass(jqLite(linkNode), oldClasses); } - linkQueue = null; - }). - error(function(response, code, headers, config) { - throw $compileMinErr('tpload', 'Failed to load template: {0}', config.url); - }); + if (afterTemplateNodeLinkFn.transcludeOnThisElement) { + childBoundTranscludeFn = createBoundTranscludeFn(scope, afterTemplateNodeLinkFn.transclude, boundTranscludeFn); + } else { + childBoundTranscludeFn = boundTranscludeFn; + } + afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, linkNode, $rootElement, + childBoundTranscludeFn); + } + linkQueue = null; + }).catch(function(error) { + if (isError(error)) { + $exceptionHandler(error); + } + }); return function delayedNodeLinkFn(ignoreChildLinkFn, scope, node, rootElement, boundTranscludeFn) { + var childBoundTranscludeFn = boundTranscludeFn; + if (scope.$$destroyed) return; if (linkQueue) { - linkQueue.push(scope); - linkQueue.push(node); - linkQueue.push(rootElement); - linkQueue.push(boundTranscludeFn); + linkQueue.push(scope, + node, + rootElement, + childBoundTranscludeFn); } else { - afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, node, rootElement, boundTranscludeFn); + if (afterTemplateNodeLinkFn.transcludeOnThisElement) { + childBoundTranscludeFn = createBoundTranscludeFn(scope, afterTemplateNodeLinkFn.transclude, boundTranscludeFn); + } + afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, node, rootElement, childBoundTranscludeFn); } }; } @@ -6439,11 +10308,18 @@ return a.index - b.index; } - function assertNoDuplicate(what, previousDirective, directive, element) { + + function wrapModuleNameIfDefined(moduleName) { + return moduleName ? + (' (module: ' + moduleName + ')') : + ''; + } + if (previousDirective) { - throw $compileMinErr('multidir', 'Multiple directives [{0}, {1}] asking for {2} on: {3}', - previousDirective.name, directive.name, what, startingTag(element)); + throw $compileMinErr('multidir', 'Multiple directives [{0}{1}, {2}{3}] asking for {4} on: {5}', + previousDirective.name, wrapModuleNameIfDefined(previousDirective.$$moduleName), + directive.name, wrapModuleNameIfDefined(directive.$$moduleName), what, startingTag(element)); } } @@ -6453,46 +10329,84 @@ if (interpolateFn) { directives.push({ priority: 0, - compile: valueFn(function textInterpolateLinkFn(scope, node) { - var parent = node.parent(), - bindings = parent.data('$binding') || []; - bindings.push(interpolateFn); - safeAddClass(parent.data('$binding', bindings), 'ng-binding'); - scope.$watch(interpolateFn, function interpolateFnWatchAction(value) { - node[0].nodeValue = value; - }); - }) + compile: function textInterpolateCompileFn(templateNode) { + var templateNodeParent = templateNode.parent(), + hasCompileParent = !!templateNodeParent.length; + + // When transcluding a template that has bindings in the root + // we don't have a parent and thus need to add the class during linking fn. + if (hasCompileParent) compile.$$addBindingClass(templateNodeParent); + + return function textInterpolateLinkFn(scope, node) { + var parent = node.parent(); + if (!hasCompileParent) compile.$$addBindingClass(parent); + compile.$$addBindingInfo(parent, interpolateFn.expressions); + scope.$watch(interpolateFn, function interpolateFnWatchAction(value) { + node[0].nodeValue = value; + }); + }; + } }); } } + function wrapTemplate(type, template) { + type = lowercase(type || 'html'); + switch (type) { + case 'svg': + case 'math': + var wrapper = window.document.createElement('div'); + wrapper.innerHTML = '<' + type + '>' + template + ''; + return wrapper.childNodes[0].childNodes; + default: + return template; + } + } + + function getTrustedContext(node, attrNormalizedName) { - if (attrNormalizedName == "srcdoc") { + if (attrNormalizedName === 'srcdoc') { return $sce.HTML; } var tag = nodeName_(node); - // maction[xlink:href] can source SVG. It's not limited to . - if (attrNormalizedName == "xlinkHref" || - (tag == "FORM" && attrNormalizedName == "action") || - (tag != "IMG" && (attrNormalizedName == "src" || - attrNormalizedName == "ngSrc"))) { + // All tags with src attributes require a RESOURCE_URL value, except for + // img and various html5 media tags. + if (attrNormalizedName === 'src' || attrNormalizedName === 'ngSrc') { + if (['img', 'video', 'audio', 'source', 'track'].indexOf(tag) === -1) { + return $sce.RESOURCE_URL; + } + // maction[xlink:href] can source SVG. It's not limited to . + } else if (attrNormalizedName === 'xlinkHref' || + (tag === 'form' && attrNormalizedName === 'action') || + // links can be stylesheets or imports, which can run script in the current origin + (tag === 'link' && attrNormalizedName === 'href') + ) { return $sce.RESOURCE_URL; } } - function addAttrInterpolateDirective(node, directives, value, name) { - var interpolateFn = $interpolate(value, true); + function addAttrInterpolateDirective(node, directives, value, name, isNgAttr) { + var trustedContext = getTrustedContext(node, name); + var mustHaveExpression = !isNgAttr; + var allOrNothing = ALL_OR_NOTHING_ATTRS[name] || isNgAttr; + + var interpolateFn = $interpolate(value, mustHaveExpression, trustedContext, allOrNothing); // no interpolation found -> ignore if (!interpolateFn) return; + if (name === 'multiple' && nodeName_(node) === 'select') { + throw $compileMinErr('selmulti', + 'Binding to the \'multiple\' attribute is not supported. Element: {0}', + startingTag(node)); + } - if (name === "multiple" && nodeName_(node) === "SELECT") { - throw $compileMinErr("selmulti", - "Binding to the 'multiple' attribute is not supported. Element: {0}", - startingTag(node)); + if (EVENT_HANDLER_ATTR_REGEXP.test(name)) { + throw $compileMinErr('nodomevents', + 'Interpolations for HTML DOM event attributes are disallowed. Please use the ' + + 'ng- versions (such as ng-click instead of onclick) instead.'); } directives.push({ @@ -6500,40 +10414,42 @@ compile: function() { return { pre: function attrInterpolatePreLinkFn(scope, element, attr) { - var $$observers = (attr.$$observers || (attr.$$observers = {})); + var $$observers = (attr.$$observers || (attr.$$observers = createMap())); - if (EVENT_HANDLER_ATTR_REGEXP.test(name)) { - throw $compileMinErr('nodomevents', - "Interpolations for HTML DOM event attributes are disallowed. Please use the " + - "ng- versions (such as ng-click instead of onclick) instead."); + // If the attribute has changed since last $interpolate()ed + var newValue = attr[name]; + if (newValue !== value) { + // we need to interpolate again since the attribute value has been updated + // (e.g. by another directive's compile function) + // ensure unset/empty values make interpolateFn falsy + interpolateFn = newValue && $interpolate(newValue, true, trustedContext, allOrNothing); + value = newValue; } - // we need to interpolate again, in case the attribute value has been updated - // (e.g. by another directive's compile function) - interpolateFn = $interpolate(attr[name], true, getTrustedContext(node, name)); - // if attribute was updated so that there is no interpolation going on we don't want to // register any observers if (!interpolateFn) return; - // TODO(i): this should likely be attr.$set(name, iterpolateFn(scope) so that we reset the - // actual attr value + // initialize attr object so that it's ready in case we need the value for isolate + // scope initialization, otherwise the value would not be available from isolate + // directive's linking fn during linking phase attr[name] = interpolateFn(scope); + ($$observers[name] || ($$observers[name] = [])).$$inter = true; (attr.$$observers && attr.$$observers[name].$$scope || scope). - $watch(interpolateFn, function interpolateFnWatchAction(newValue, oldValue) { - //special case for class attribute addition + removal - //so that class changes can tap into the animation - //hooks provided by the $animate service. Be sure to - //skip animations when the first digest occurs (when - //both the new and the old values are the same) since - //the CSS classes are the non-interpolated values - if(name === 'class' && newValue != oldValue) { - attr.$updateClass(newValue, oldValue); - } else { - attr.$set(name, newValue); - } - }); + $watch(interpolateFn, function interpolateFnWatchAction(newValue, oldValue) { + //special case for class attribute addition + removal + //so that class changes can tap into the animation + //hooks provided by the $animate service. Be sure to + //skip animations when the first digest occurs (when + //both the new and the old values are the same) since + //the CSS classes are the non-interpolated values + if (name === 'class' && newValue !== oldValue) { + attr.$updateClass(newValue, oldValue); + } else { + attr.$set(name, newValue); + } + }); } }; } @@ -6553,16 +10469,16 @@ */ function replaceWith($rootElement, elementsToRemove, newNode) { var firstElementToRemove = elementsToRemove[0], - removeCount = elementsToRemove.length, - parent = firstElementToRemove.parentNode, - i, ii; + removeCount = elementsToRemove.length, + parent = firstElementToRemove.parentNode, + i, ii; if ($rootElement) { - for(i = 0, ii = $rootElement.length; i < ii; i++) { - if ($rootElement[i] == firstElementToRemove) { + for (i = 0, ii = $rootElement.length; i < ii; i++) { + if ($rootElement[i] === firstElementToRemove) { $rootElement[i++] = newNode; for (var j = i, j2 = j + removeCount - 1, - jj = $rootElement.length; + jj = $rootElement.length; j < jj; j++, j2++) { if (j2 < jj) { $rootElement[j] = $rootElement[j2]; @@ -6571,6 +10487,13 @@ } } $rootElement.length -= removeCount - 1; + + // If the replaced element is also the jQuery .context then replace it + // .context is a deprecated jQuery api, so we should set it only when jQuery set it + // http://api.jquery.com/context/ + if ($rootElement.context === firstElementToRemove) { + $rootElement.context = newNode; + } break; } } @@ -6579,16 +10502,34 @@ if (parent) { parent.replaceChild(newNode, firstElementToRemove); } - var fragment = document.createDocumentFragment(); - fragment.appendChild(firstElementToRemove); - newNode[jqLite.expando] = firstElementToRemove[jqLite.expando]; - for (var k = 1, kk = elementsToRemove.length; k < kk; k++) { - var element = elementsToRemove[k]; - jqLite(element).remove(); // must do this way to clean up expando - fragment.appendChild(element); - delete elementsToRemove[k]; + + // Append all the `elementsToRemove` to a fragment. This will... + // - remove them from the DOM + // - allow them to still be traversed with .nextSibling + // - allow a single fragment.qSA to fetch all elements being removed + var fragment = window.document.createDocumentFragment(); + for (i = 0; i < removeCount; i++) { + fragment.appendChild(elementsToRemove[i]); } + if (jqLite.hasData(firstElementToRemove)) { + // Copy over user data (that includes Angular's $scope etc.). Don't copy private + // data here because there's no public interface in jQuery to do that and copying over + // event listeners (which is the main use of private data) wouldn't work anyway. + jqLite.data(newNode, jqLite.data(firstElementToRemove)); + + // Remove $destroy event listeners from `firstElementToRemove` + jqLite(firstElementToRemove).off('$destroy'); + } + + // Cleanup any data/listeners on the elements and children. + // This includes invoking the $destroy event on any elements with listeners. + jqLite.cleanData(fragment.querySelectorAll('*')); + + // Update the jqLite collection to only contain the `newNode` + for (i = 1; i < removeCount; i++) { + delete elementsToRemove[i]; + } elementsToRemove[0] = newNode; elementsToRemove.length = 1; } @@ -6597,51 +10538,244 @@ function cloneAndAnnotateFn(fn, annotation) { return extend(function() { return fn.apply(null, arguments); }, fn, annotation); } + + + function invokeLinkFn(linkFn, scope, $element, attrs, controllers, transcludeFn) { + try { + linkFn(scope, $element, attrs, controllers, transcludeFn); + } catch (e) { + $exceptionHandler(e, startingTag($element)); + } + } + + function strictBindingsCheck(attrName, directiveName) { + if (strictComponentBindingsEnabled) { + throw $compileMinErr('missingattr', + 'Attribute \'{0}\' of \'{1}\' is non-optional and must be set!', + attrName, directiveName); + } + } + + // Set up $watches for isolate scope and controller bindings. + function initializeDirectiveBindings(scope, attrs, destination, bindings, directive) { + var removeWatchCollection = []; + var initialChanges = {}; + var changes; + + forEach(bindings, function initializeBinding(definition, scopeName) { + var attrName = definition.attrName, + optional = definition.optional, + mode = definition.mode, // @, =, <, or & + lastValue, + parentGet, parentSet, compare, removeWatch; + + switch (mode) { + + case '@': + if (!optional && !hasOwnProperty.call(attrs, attrName)) { + strictBindingsCheck(attrName, directive.name); + destination[scopeName] = attrs[attrName] = undefined; + + } + removeWatch = attrs.$observe(attrName, function(value) { + if (isString(value) || isBoolean(value)) { + var oldValue = destination[scopeName]; + recordChanges(scopeName, value, oldValue); + destination[scopeName] = value; + } + }); + attrs.$$observers[attrName].$$scope = scope; + lastValue = attrs[attrName]; + if (isString(lastValue)) { + // If the attribute has been provided then we trigger an interpolation to ensure + // the value is there for use in the link fn + destination[scopeName] = $interpolate(lastValue)(scope); + } else if (isBoolean(lastValue)) { + // If the attributes is one of the BOOLEAN_ATTR then Angular will have converted + // the value to boolean rather than a string, so we special case this situation + destination[scopeName] = lastValue; + } + initialChanges[scopeName] = new SimpleChange(_UNINITIALIZED_VALUE, destination[scopeName]); + removeWatchCollection.push(removeWatch); + break; + + case '=': + if (!hasOwnProperty.call(attrs, attrName)) { + if (optional) break; + strictBindingsCheck(attrName, directive.name); + attrs[attrName] = undefined; + } + if (optional && !attrs[attrName]) break; + + parentGet = $parse(attrs[attrName]); + if (parentGet.literal) { + compare = equals; + } else { + compare = simpleCompare; + } + parentSet = parentGet.assign || function() { + // reset the change, or we will throw this exception on every $digest + lastValue = destination[scopeName] = parentGet(scope); + throw $compileMinErr('nonassign', + 'Expression \'{0}\' in attribute \'{1}\' used with directive \'{2}\' is non-assignable!', + attrs[attrName], attrName, directive.name); + }; + lastValue = destination[scopeName] = parentGet(scope); + var parentValueWatch = function parentValueWatch(parentValue) { + if (!compare(parentValue, destination[scopeName])) { + // we are out of sync and need to copy + if (!compare(parentValue, lastValue)) { + // parent changed and it has precedence + destination[scopeName] = parentValue; + } else { + // if the parent can be assigned then do so + parentSet(scope, parentValue = destination[scopeName]); + } + } + lastValue = parentValue; + return lastValue; + }; + parentValueWatch.$stateful = true; + if (definition.collection) { + removeWatch = scope.$watchCollection(attrs[attrName], parentValueWatch); + } else { + removeWatch = scope.$watch($parse(attrs[attrName], parentValueWatch), null, parentGet.literal); + } + removeWatchCollection.push(removeWatch); + break; + + case '<': + if (!hasOwnProperty.call(attrs, attrName)) { + if (optional) break; + strictBindingsCheck(attrName, directive.name); + attrs[attrName] = undefined; + } + if (optional && !attrs[attrName]) break; + + parentGet = $parse(attrs[attrName]); + var deepWatch = parentGet.literal; + + var initialValue = destination[scopeName] = parentGet(scope); + initialChanges[scopeName] = new SimpleChange(_UNINITIALIZED_VALUE, destination[scopeName]); + + removeWatch = scope.$watch(parentGet, function parentValueWatchAction(newValue, oldValue) { + if (oldValue === newValue) { + if (oldValue === initialValue || (deepWatch && equals(oldValue, initialValue))) { + return; + } + oldValue = initialValue; + } + recordChanges(scopeName, newValue, oldValue); + destination[scopeName] = newValue; + }, deepWatch); + + removeWatchCollection.push(removeWatch); + break; + + case '&': + if (!optional && !hasOwnProperty.call(attrs, attrName)) { + strictBindingsCheck(attrName, directive.name); + } + // Don't assign Object.prototype method to scope + parentGet = attrs.hasOwnProperty(attrName) ? $parse(attrs[attrName]) : noop; + + // Don't assign noop to destination if expression is not valid + if (parentGet === noop && optional) break; + + destination[scopeName] = function(locals) { + return parentGet(scope, locals); + }; + break; + } + }); + + function recordChanges(key, currentValue, previousValue) { + if (isFunction(destination.$onChanges) && !simpleCompare(currentValue, previousValue)) { + // If we have not already scheduled the top level onChangesQueue handler then do so now + if (!onChangesQueue) { + scope.$$postDigest(flushOnChangesQueue); + onChangesQueue = []; + } + // If we have not already queued a trigger of onChanges for this controller then do so now + if (!changes) { + changes = {}; + onChangesQueue.push(triggerOnChangesHook); + } + // If the has been a change on this property already then we need to reuse the previous value + if (changes[key]) { + previousValue = changes[key].previousValue; + } + // Store this change + changes[key] = new SimpleChange(previousValue, currentValue); + } + } + + function triggerOnChangesHook() { + destination.$onChanges(changes); + // Now clear the changes so that we schedule onChanges when more changes arrive + changes = undefined; + } + + return { + initialChanges: initialChanges, + removeWatches: removeWatchCollection.length && function removeWatches() { + for (var i = 0, ii = removeWatchCollection.length; i < ii; ++i) { + removeWatchCollection[i](); + } + } + }; + } }]; } - var PREFIX_REGEXP = /^(x[\:\-_]|data[\:\-_])/i; + function SimpleChange(previous, current) { + this.previousValue = previous; + this.currentValue = current; + } + SimpleChange.prototype.isFirstChange = function() { return this.previousValue === _UNINITIALIZED_VALUE; }; + + + var PREFIX_REGEXP = /^((?:x|data)[:\-_])/i; + var SPECIAL_CHARS_REGEXP = /[:\-_]+(.)/g; + /** * Converts all accepted directives format into proper directive name. - * All of these will become 'myDirective': - * my:Directive - * my-directive - * x-my-directive - * data-my:directive - * - * Also there is special case for Moz prefix starting with upper case letter. * @param name Name to normalize */ function directiveNormalize(name) { - return camelCase(name.replace(PREFIX_REGEXP, '')); + return name + .replace(PREFIX_REGEXP, '') + .replace(SPECIAL_CHARS_REGEXP, fnCamelCaseReplace); } /** - * @ngdoc object - * @name ng.$compile.directive.Attributes + * @ngdoc type + * @name $compile.directive.Attributes * * @description * A shared object between directive compile / linking functions which contains normalized DOM * element attributes. The values reflect current binding state `{{ }}`. The normalization is * needed since all of these are treated as equivalent in Angular: * + * ``` * + * ``` */ /** * @ngdoc property - * @name ng.$compile.directive.Attributes#$attr - * @propertyOf ng.$compile.directive.Attributes - * @returns {object} A map of DOM element attribute names to the normalized name. This is - * needed to do reverse lookup from normalized name back to actual name. + * @name $compile.directive.Attributes#$attr + * + * @description + * A map of DOM element attribute names to the normalized name. This is + * needed to do reverse lookup from normalized name back to actual name. */ /** - * @ngdoc function - * @name ng.$compile.directive.Attributes#$set - * @methodOf ng.$compile.directive.Attributes - * @function + * @ngdoc method + * @name $compile.directive.Attributes#$set + * @kind function * * @description * Set DOM element attribute value. @@ -6664,7 +10798,7 @@ /* NodeList */ nodeList, /* Element */ rootElement, /* function(Function) */ boundTranscludeFn - ){} + ) {} function directiveLinkingFn( /* nodesetLinkingFn */ nodesetLinkingFn, @@ -6672,43 +10806,83 @@ /* Node */ node, /* Element */ rootElement, /* function(Function) */ boundTranscludeFn - ){} + ) {} function tokenDifference(str1, str2) { var values = '', - tokens1 = str1.split(/\s+/), - tokens2 = str2.split(/\s+/); + tokens1 = str1.split(/\s+/), + tokens2 = str2.split(/\s+/); outer: - for(var i = 0; i < tokens1.length; i++) { - var token = tokens1[i]; - for(var j = 0; j < tokens2.length; j++) { - if(token == tokens2[j]) continue outer; - } - values += (values.length > 0 ? ' ' : '') + token; + for (var i = 0; i < tokens1.length; i++) { + var token = tokens1[i]; + for (var j = 0; j < tokens2.length; j++) { + if (token === tokens2[j]) continue outer; } + values += (values.length > 0 ? ' ' : '') + token; + } return values; } + function removeComments(jqNodes) { + jqNodes = jqLite(jqNodes); + var i = jqNodes.length; + + if (i <= 1) { + return jqNodes; + } + + while (i--) { + var node = jqNodes[i]; + if (node.nodeType === NODE_TYPE_COMMENT || + (node.nodeType === NODE_TYPE_TEXT && node.nodeValue.trim() === '')) { + splice.call(jqNodes, i, 1); + } + } + return jqNodes; + } + + var $controllerMinErr = minErr('$controller'); + + + var CNTRL_REG = /^(\S+)(\s+as\s+([\w$]+))?$/; + function identifierForController(controller, ident) { + if (ident && isString(ident)) return ident; + if (isString(controller)) { + var match = CNTRL_REG.exec(controller); + if (match) return match[3]; + } + } + + /** - * @ngdoc object - * @name ng.$controllerProvider + * @ngdoc provider + * @name $controllerProvider + * @this + * * @description * The {@link ng.$controller $controller service} is used by Angular to create new * controllers. * * This provider allows controller registration via the - * {@link ng.$controllerProvider#methods_register register} method. + * {@link ng.$controllerProvider#register register} method. */ function $ControllerProvider() { var controllers = {}, - CNTRL_REG = /^(\S+)(\s+as\s+(\w+))?$/; - + globals = false; /** - * @ngdoc function - * @name ng.$controllerProvider#register - * @methodOf ng.$controllerProvider + * @ngdoc method + * @name $controllerProvider#has + * @param {string} name Controller name to check. + */ + this.has = function(name) { + return controllers.hasOwnProperty(name); + }; + + /** + * @ngdoc method + * @name $controllerProvider#register * @param {string|Object} name Controller name, or an object map of controllers where the keys are * the names and the values are the constructors. * @param {Function|Array} constructor Controller constructor fn (optionally decorated with DI @@ -6723,12 +10897,26 @@ } }; + /** + * @ngdoc method + * @name $controllerProvider#allowGlobals + * @description If called, allows `$controller` to find controller constructors on `window` + * + * @deprecated + * sinceVersion="v1.3.0" + * removeVersion="v1.7.0" + * This method of finding controllers has been deprecated. + */ + this.allowGlobals = function() { + globals = true; + }; + this.$get = ['$injector', '$window', function($injector, $window) { /** - * @ngdoc function - * @name ng.$controller + * @ngdoc service + * @name $controller * @requires $injector * * @param {Function|string} constructor If called with a function then it's considered to be the @@ -6737,7 +10925,12 @@ * * * check if a controller with given name is registered via `$controllerProvider` * * check if evaluating the string on the current scope returns a constructor - * * check `window[constructor]` on the global `window` object + * * if $controllerProvider#allowGlobals, check `window[constructor]` on the global + * `window` object (deprecated, not recommended) + * + * The string can use the `controller as property` syntax, where the controller instance is published + * as the specified property on the `scope`; the `scope` must be injected into `locals` param for this + * to work correctly. * * @param {Object} locals Injection locals for Controller. * @return {Object} Instance of given controller. @@ -6745,59 +10938,165 @@ * @description * `$controller` service is responsible for instantiating controllers. * - * It's just a simple call to {@link AUTO.$injector $injector}, but extracted into - * a service, so that one can override this service with {@link https://gist.github.com/1649788 - * BC version}. + * It's just a simple call to {@link auto.$injector $injector}, but extracted into + * a service, so that one can override this service with [BC version](https://gist.github.com/1649788). */ - return function(expression, locals) { + return function $controller(expression, locals, later, ident) { + // PRIVATE API: + // param `later` --- indicates that the controller's constructor is invoked at a later time. + // If true, $controller will allocate the object with the correct + // prototype chain, but will not invoke the controller until a returned + // callback is invoked. + // param `ident` --- An optional label which overrides the label parsed from the controller + // expression, if any. var instance, match, constructor, identifier; + later = later === true; + if (ident && isString(ident)) { + identifier = ident; + } - if(isString(expression)) { - match = expression.match(CNTRL_REG), - constructor = match[1], - identifier = match[3]; + if (isString(expression)) { + match = expression.match(CNTRL_REG); + if (!match) { + throw $controllerMinErr('ctrlfmt', + 'Badly formed controller string \'{0}\'. ' + + 'Must match `__name__ as __id__` or `__name__`.', expression); + } + constructor = match[1]; + identifier = identifier || match[3]; expression = controllers.hasOwnProperty(constructor) - ? controllers[constructor] - : getter(locals.$scope, constructor, true) || getter($window, constructor, true); + ? controllers[constructor] + : getter(locals.$scope, constructor, true) || + (globals ? getter($window, constructor, true) : undefined); + + if (!expression) { + throw $controllerMinErr('ctrlreg', + 'The controller with the name \'{0}\' is not registered.', constructor); + } assertArgFn(expression, constructor, true); } - instance = $injector.instantiate(expression, locals); + if (later) { + // Instantiate controller later: + // This machinery is used to create an instance of the object before calling the + // controller's constructor itself. + // + // This allows properties to be added to the controller before the constructor is + // invoked. Primarily, this is used for isolate scope bindings in $compile. + // + // This feature is not intended for use by applications, and is thus not documented + // publicly. + // Object creation: http://jsperf.com/create-constructor/2 + var controllerPrototype = (isArray(expression) ? + expression[expression.length - 1] : expression).prototype; + instance = Object.create(controllerPrototype || null); - if (identifier) { - if (!(locals && typeof locals.$scope == 'object')) { - throw minErr('$controller')('noscp', - "Cannot export controller '{0}' as '{1}'! No $scope object provided via `locals`.", - constructor || expression.name, identifier); + if (identifier) { + addIdentifier(locals, identifier, instance, constructor || expression.name); } - locals.$scope[identifier] = instance; + return extend(function $controllerInit() { + var result = $injector.invoke(expression, instance, locals, constructor); + if (result !== instance && (isObject(result) || isFunction(result))) { + instance = result; + if (identifier) { + // If result changed, re-assign controllerAs value to scope. + addIdentifier(locals, identifier, instance, constructor || expression.name); + } + } + return instance; + }, { + instance: instance, + identifier: identifier + }); + } + + instance = $injector.instantiate(expression, locals, constructor); + + if (identifier) { + addIdentifier(locals, identifier, instance, constructor || expression.name); } return instance; }; + + function addIdentifier(locals, identifier, instance, name) { + if (!(locals && isObject(locals.$scope))) { + throw minErr('$controller')('noscp', + 'Cannot export controller \'{0}\' as \'{1}\'! No $scope object provided via `locals`.', + name, identifier); + } + + locals.$scope[identifier] = instance; + } + }]; + } + + /** + * @ngdoc service + * @name $document + * @requires $window + * @this + * + * @description + * A {@link angular.element jQuery or jqLite} wrapper for the browser's `window.document` object. + * + * @example + + +
    +

    $document title:

    +

    window.document title:

    +
    +
    + + angular.module('documentExample', []) + .controller('ExampleController', ['$scope', '$document', function($scope, $document) { + $scope.title = $document[0].title; + $scope.windowTitle = angular.element(window.document)[0].title; + }]); + +
    + */ + function $DocumentProvider() { + this.$get = ['$window', function(window) { + return jqLite(window.document); + }]; + } + + + /** + * @private + * @this + * Listens for document visibility change and makes the current status accessible. + */ + function $$IsDocumentHiddenProvider() { + this.$get = ['$document', '$rootScope', function($document, $rootScope) { + var doc = $document[0]; + var hidden = doc && doc.hidden; + + $document.on('visibilitychange', changeListener); + + $rootScope.$on('$destroy', function() { + $document.off('visibilitychange', changeListener); + }); + + function changeListener() { + hidden = doc.hidden; + } + + return function() { + return hidden; + }; }]; } /** - * @ngdoc object - * @name ng.$document - * @requires $window - * - * @description - * A {@link angular.element jQuery or jqLite} wrapper for the browser's `window.document` object. - */ - function $DocumentProvider(){ - this.$get = ['$window', function(window){ - return jqLite(window.document); - }]; - } - - /** - * @ngdoc function - * @name ng.$exceptionHandler - * @requires $log + * @ngdoc service + * @name $exceptionHandler + * @requires ng.$log + * @this * * @description * Any uncaught exception in angular expressions is delegated to this service. @@ -6809,20 +11108,31 @@ * * ## Example: * - *
    -   *   angular.module('exceptionOverride', []).factory('$exceptionHandler', function () {
    - *     return function (exception, cause) {
    - *       exception.message += ' (caused by "' + cause + '")';
    - *       throw exception;
    - *     };
    - *   });
    -   * 
    + * The example below will overwrite the default `$exceptionHandler` in order to (a) log uncaught + * errors to the backend for later inspection by the developers and (b) to use `$log.warn()` instead + * of `$log.error()`. * - * This example will override the normal action of `$exceptionHandler`, to make angular - * exceptions fail hard when they happen, instead of just logging to the console. + * ```js + * angular. + * module('exceptionOverwrite', []). + * factory('$exceptionHandler', ['$log', 'logErrorsToBackend', function($log, logErrorsToBackend) { + * return function myExceptionHandler(exception, cause) { + * logErrorsToBackend(exception, cause); + * $log.warn(exception, cause); + * }; + * }]); + * ``` + * + *
    + * Note, that code executed in event-listeners (even those registered using jqLite's `on`/`bind` + * methods) does not delegate exceptions to the {@link ng.$exceptionHandler $exceptionHandler} + * (unless executed during a digest). + * + * If you wish, you can manually delegate exceptions, e.g. + * `try { ... } catch(e) { $exceptionHandler(e); }` * * @param {Error} exception Exception associated with the error. - * @param {string=} cause optional information about the context in which + * @param {string=} cause Optional information about the context in which * the error was thrown. * */ @@ -6834,6 +11144,190 @@ }]; } + var $$ForceReflowProvider = /** @this */ function() { + this.$get = ['$document', function($document) { + return function(domNode) { + //the line below will force the browser to perform a repaint so + //that all the animated elements within the animation frame will + //be properly updated and drawn on screen. This is required to + //ensure that the preparation animation is properly flushed so that + //the active state picks up from there. DO NOT REMOVE THIS LINE. + //DO NOT OPTIMIZE THIS LINE. THE MINIFIER WILL REMOVE IT OTHERWISE WHICH + //WILL RESULT IN AN UNPREDICTABLE BUG THAT IS VERY HARD TO TRACK DOWN AND + //WILL TAKE YEARS AWAY FROM YOUR LIFE. + if (domNode) { + if (!domNode.nodeType && domNode instanceof jqLite) { + domNode = domNode[0]; + } + } else { + domNode = $document[0].body; + } + return domNode.offsetWidth + 1; + }; + }]; + }; + + var APPLICATION_JSON = 'application/json'; + var CONTENT_TYPE_APPLICATION_JSON = {'Content-Type': APPLICATION_JSON + ';charset=utf-8'}; + var JSON_START = /^\[|^\{(?!\{)/; + var JSON_ENDS = { + '[': /]$/, + '{': /}$/ + }; + var JSON_PROTECTION_PREFIX = /^\)]\}',?\n/; + var $httpMinErr = minErr('$http'); + + function serializeValue(v) { + if (isObject(v)) { + return isDate(v) ? v.toISOString() : toJson(v); + } + return v; + } + + + /** @this */ + function $HttpParamSerializerProvider() { + /** + * @ngdoc service + * @name $httpParamSerializer + * @description + * + * Default {@link $http `$http`} params serializer that converts objects to strings + * according to the following rules: + * + * * `{'foo': 'bar'}` results in `foo=bar` + * * `{'foo': Date.now()}` results in `foo=2015-04-01T09%3A50%3A49.262Z` (`toISOString()` and encoded representation of a Date object) + * * `{'foo': ['bar', 'baz']}` results in `foo=bar&foo=baz` (repeated key for each array element) + * * `{'foo': {'bar':'baz'}}` results in `foo=%7B%22bar%22%3A%22baz%22%7D` (stringified and encoded representation of an object) + * + * Note that serializer will sort the request parameters alphabetically. + * */ + + this.$get = function() { + return function ngParamSerializer(params) { + if (!params) return ''; + var parts = []; + forEachSorted(params, function(value, key) { + if (value === null || isUndefined(value) || isFunction(value)) return; + if (isArray(value)) { + forEach(value, function(v) { + parts.push(encodeUriQuery(key) + '=' + encodeUriQuery(serializeValue(v))); + }); + } else { + parts.push(encodeUriQuery(key) + '=' + encodeUriQuery(serializeValue(value))); + } + }); + + return parts.join('&'); + }; + }; + } + + /** @this */ + function $HttpParamSerializerJQLikeProvider() { + /** + * @ngdoc service + * @name $httpParamSerializerJQLike + * + * @description + * + * Alternative {@link $http `$http`} params serializer that follows + * jQuery's [`param()`](http://api.jquery.com/jquery.param/) method logic. + * The serializer will also sort the params alphabetically. + * + * To use it for serializing `$http` request parameters, set it as the `paramSerializer` property: + * + * ```js + * $http({ + * url: myUrl, + * method: 'GET', + * params: myParams, + * paramSerializer: '$httpParamSerializerJQLike' + * }); + * ``` + * + * It is also possible to set it as the default `paramSerializer` in the + * {@link $httpProvider#defaults `$httpProvider`}. + * + * Additionally, you can inject the serializer and use it explicitly, for example to serialize + * form data for submission: + * + * ```js + * .controller(function($http, $httpParamSerializerJQLike) { + * //... + * + * $http({ + * url: myUrl, + * method: 'POST', + * data: $httpParamSerializerJQLike(myData), + * headers: { + * 'Content-Type': 'application/x-www-form-urlencoded' + * } + * }); + * + * }); + * ``` + * + * */ + this.$get = function() { + return function jQueryLikeParamSerializer(params) { + if (!params) return ''; + var parts = []; + serialize(params, '', true); + return parts.join('&'); + + function serialize(toSerialize, prefix, topLevel) { + if (toSerialize === null || isUndefined(toSerialize)) return; + if (isArray(toSerialize)) { + forEach(toSerialize, function(value, index) { + serialize(value, prefix + '[' + (isObject(value) ? index : '') + ']'); + }); + } else if (isObject(toSerialize) && !isDate(toSerialize)) { + forEachSorted(toSerialize, function(value, key) { + serialize(value, prefix + + (topLevel ? '' : '[') + + key + + (topLevel ? '' : ']')); + }); + } else { + parts.push(encodeUriQuery(prefix) + '=' + encodeUriQuery(serializeValue(toSerialize))); + } + } + }; + }; + } + + function defaultHttpResponseTransform(data, headers) { + if (isString(data)) { + // Strip json vulnerability protection prefix and trim whitespace + var tempData = data.replace(JSON_PROTECTION_PREFIX, '').trim(); + + if (tempData) { + var contentType = headers('Content-Type'); + var hasJsonContentType = contentType && (contentType.indexOf(APPLICATION_JSON) === 0); + + if (hasJsonContentType || isJsonLike(tempData)) { + try { + data = fromJson(tempData); + } catch (e) { + if (!hasJsonContentType) { + return data; + } + throw $httpMinErr('baddata', 'Data must be a valid JSON object. Received: "{0}". ' + + 'Parse error: "{1}"', data, e); + } + } + } + } + + return data; + } + + function isJsonLike(str) { + var jsonStart = str.match(JSON_START); + return jsonStart && JSON_ENDS[jsonStart[0]].test(str); + } + /** * Parse headers into key value object * @@ -6841,23 +11335,24 @@ * @returns {Object} Parsed headers as key value object */ function parseHeaders(headers) { - var parsed = {}, key, val, i; - - if (!headers) return parsed; - - forEach(headers.split('\n'), function(line) { - i = line.indexOf(':'); - key = lowercase(trim(line.substr(0, i))); - val = trim(line.substr(i + 1)); + var parsed = createMap(), i; + function fillInParsed(key, val) { if (key) { - if (parsed[key]) { - parsed[key] += ', ' + val; - } else { - parsed[key] = val; - } + parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val; } - }); + } + + if (isString(headers)) { + forEach(headers.split('\n'), function(line) { + i = line.indexOf(':'); + fillInParsed(lowercase(trim(line.substr(0, i))), trim(line.substr(i + 1))); + }); + } else if (isObject(headers)) { + forEach(headers, function(headerVal, headerKey) { + fillInParsed(lowercase(headerKey), trim(headerVal)); + }); + } return parsed; } @@ -6872,17 +11367,21 @@ * @param {(string|Object)} headers Headers to provide access to. * @returns {function(string=)} Returns a getter function which if called with: * - * - if called with single an argument returns a single header value or null + * - if called with an argument returns a single header value or null * - if called with no arguments returns an object containing all headers. */ function headersGetter(headers) { - var headersObj = isObject(headers) ? headers : undefined; + var headersObj; return function(name) { if (!headersObj) headersObj = parseHeaders(headers); if (name) { - return headersObj[lowercase(name)] || null; + var value = headersObj[lowercase(name)]; + if (value === undefined) { + value = null; + } + return value; } return headersObj; @@ -6896,16 +11395,18 @@ * This function is used for both request and response transforming * * @param {*} data Data to transform. - * @param {function(string=)} headers Http headers getter fn. - * @param {(function|Array.)} fns Function or an array of functions. + * @param {function(string=)} headers HTTP headers getter fn. + * @param {number} status HTTP status code of the response. + * @param {(Function|Array.)} fns Function or an array of functions. * @returns {*} Transformed data. */ - function transformData(data, headers, fns) { - if (isFunction(fns)) - return fns(data, headers); + function transformData(data, headers, status, fns) { + if (isFunction(fns)) { + return fns(data, headers, status); + } forEach(fns, function(fn) { - data = fn(data, headers); + data = fn(data, headers, status); }); return data; @@ -6917,27 +11418,75 @@ } + /** + * @ngdoc provider + * @name $httpProvider + * @this + * + * @description + * Use `$httpProvider` to change the default behavior of the {@link ng.$http $http} service. + * */ function $HttpProvider() { - var JSON_START = /^\s*(\[|\{[^\{])/, - JSON_END = /[\}\]]\s*$/, - PROTECTION_PREFIX = /^\)\]\}',?\n/, - CONTENT_TYPE_APPLICATION_JSON = {'Content-Type': 'application/json;charset=utf-8'}; - + /** + * @ngdoc property + * @name $httpProvider#defaults + * @description + * + * Object containing default values for all {@link ng.$http $http} requests. + * + * - **`defaults.cache`** - {boolean|Object} - A boolean value or object created with + * {@link ng.$cacheFactory `$cacheFactory`} to enable or disable caching of HTTP responses + * by default. See {@link $http#caching $http Caching} for more information. + * + * - **`defaults.headers`** - {Object} - Default headers for all $http requests. + * Refer to {@link ng.$http#setting-http-headers $http} for documentation on + * setting default headers. + * - **`defaults.headers.common`** + * - **`defaults.headers.post`** + * - **`defaults.headers.put`** + * - **`defaults.headers.patch`** + * + * - **`defaults.jsonpCallbackParam`** - `{string}` - the name of the query parameter that passes the name of the + * callback in a JSONP request. The value of this parameter will be replaced with the expression generated by the + * {@link $jsonpCallbacks} service. Defaults to `'callback'`. + * + * - **`defaults.paramSerializer`** - `{string|function(Object):string}` - A function + * used to the prepare string representation of request parameters (specified as an object). + * If specified as string, it is interpreted as a function registered with the {@link auto.$injector $injector}. + * Defaults to {@link ng.$httpParamSerializer $httpParamSerializer}. + * + * - **`defaults.transformRequest`** - + * `{Array|function(data, headersGetter)}` - + * An array of functions (or a single function) which are applied to the request data. + * By default, this is an array with one request transformation function: + * + * - If the `data` property of the request configuration object contains an object, serialize it + * into JSON format. + * + * - **`defaults.transformResponse`** - + * `{Array|function(data, headersGetter, status)}` - + * An array of functions (or a single function) which are applied to the response data. By default, + * this is an array which applies one response transformation function that does two things: + * + * - If XSRF prefix is detected, strip it + * (see {@link ng.$http#security-considerations Security Considerations in the $http docs}). + * - If the `Content-Type` is `application/json` or the response looks like JSON, + * deserialize it using a JSON parser. + * + * - **`defaults.xsrfCookieName`** - {string} - Name of cookie containing the XSRF token. + * Defaults value is `'XSRF-TOKEN'`. + * + * - **`defaults.xsrfHeaderName`** - {string} - Name of HTTP header to populate with the + * XSRF token. Defaults value is `'X-XSRF-TOKEN'`. + * + **/ var defaults = this.defaults = { // transform incoming response data - transformResponse: [function(data) { - if (isString(data)) { - // strip json vulnerability protection prefix - data = data.replace(PROTECTION_PREFIX, ''); - if (JSON_START.test(data) && JSON_END.test(data)) - data = fromJson(data); - } - return data; - }], + transformResponse: [defaultHttpResponseTransform], // transform outgoing request data transformRequest: [function(d) { - return isObject(d) && !isFile(d) ? toJson(d) : d; + return isObject(d) && !isFile(d) && !isBlob(d) && !isFormData(d) ? toJson(d) : d; }], // default headers @@ -6945,32 +11494,73 @@ common: { 'Accept': 'application/json, text/plain, */*' }, - post: CONTENT_TYPE_APPLICATION_JSON, - put: CONTENT_TYPE_APPLICATION_JSON, - patch: CONTENT_TYPE_APPLICATION_JSON + post: shallowCopy(CONTENT_TYPE_APPLICATION_JSON), + put: shallowCopy(CONTENT_TYPE_APPLICATION_JSON), + patch: shallowCopy(CONTENT_TYPE_APPLICATION_JSON) }, xsrfCookieName: 'XSRF-TOKEN', - xsrfHeaderName: 'X-XSRF-TOKEN' + xsrfHeaderName: 'X-XSRF-TOKEN', + + paramSerializer: '$httpParamSerializer', + + jsonpCallbackParam: 'callback' + }; + + var useApplyAsync = false; + /** + * @ngdoc method + * @name $httpProvider#useApplyAsync + * @description + * + * Configure $http service to combine processing of multiple http responses received at around + * the same time via {@link ng.$rootScope.Scope#$applyAsync $rootScope.$applyAsync}. This can result in + * significant performance improvement for bigger applications that make many HTTP requests + * concurrently (common during application bootstrap). + * + * Defaults to false. If no value is specified, returns the current configured value. + * + * @param {boolean=} value If true, when requests are loaded, they will schedule a deferred + * "apply" on the next tick, giving time for subsequent requests in a roughly ~10ms window + * to load and share the same digest cycle. + * + * @returns {boolean|Object} If a value is specified, returns the $httpProvider for chaining. + * otherwise, returns the current configured value. + **/ + this.useApplyAsync = function(value) { + if (isDefined(value)) { + useApplyAsync = !!value; + return this; + } + return useApplyAsync; }; /** - * Are ordered by request, i.e. they are applied in the same order as the + * @ngdoc property + * @name $httpProvider#interceptors + * @description + * + * Array containing service factories for all synchronous or asynchronous {@link ng.$http $http} + * pre-processing of request or postprocessing of responses. + * + * These service factories are ordered by request, i.e. they are applied in the same order as the * array, on request, but reverse order, on response. - */ + * + * {@link ng.$http#interceptors Interceptors detailed info} + **/ var interceptorFactories = this.interceptors = []; - /** - * For historical reasons, response interceptors are ordered by the order in which - * they are applied to the response. (This is the opposite of interceptorFactories) - */ - var responseInterceptorFactories = this.responseInterceptors = []; - - this.$get = ['$httpBackend', '$browser', '$cacheFactory', '$rootScope', '$q', '$injector', - function($httpBackend, $browser, $cacheFactory, $rootScope, $q, $injector) { + this.$get = ['$browser', '$httpBackend', '$$cookieReader', '$cacheFactory', '$rootScope', '$q', '$injector', '$sce', + function($browser, $httpBackend, $$cookieReader, $cacheFactory, $rootScope, $q, $injector, $sce) { var defaultCache = $cacheFactory('$http'); + /** + * Make sure that default param serializer is exposed as a function + */ + defaults.paramSerializer = isString(defaults.paramSerializer) ? + $injector.get(defaults.paramSerializer) : defaults.paramSerializer; + /** * Interceptors stored in reverse order. Inner interceptors before outer interceptors. * The reversal is needed so that we can build up the interception chain around the @@ -6980,35 +11570,14 @@ forEach(interceptorFactories, function(interceptorFactory) { reversedInterceptors.unshift(isString(interceptorFactory) - ? $injector.get(interceptorFactory) : $injector.invoke(interceptorFactory)); + ? $injector.get(interceptorFactory) : $injector.invoke(interceptorFactory)); }); - forEach(responseInterceptorFactories, function(interceptorFactory, index) { - var responseFn = isString(interceptorFactory) - ? $injector.get(interceptorFactory) - : $injector.invoke(interceptorFactory); - - /** - * Response interceptors go before "around" interceptors (no real reason, just - * had to pick one.) But they are already reversed, so we can't use unshift, hence - * the splice. - */ - reversedInterceptors.splice(index, 0, { - response: function(response) { - return responseFn($q.when(response)); - }, - responseError: function(response) { - return responseFn($q.reject(response)); - } - }); - }); - - /** - * @ngdoc function - * @name ng.$http - * @requires $httpBackend - * @requires $browser + * @ngdoc service + * @kind function + * @name $http + * @requires ng.$httpBackend * @requires $cacheFactory * @requires $rootScope * @requires $q @@ -7016,104 +11585,98 @@ * * @description * The `$http` service is a core Angular service that facilitates communication with the remote - * HTTP servers via the browser's {@link https://developer.mozilla.org/en/xmlhttprequest - * XMLHttpRequest} object or via {@link http://en.wikipedia.org/wiki/JSONP JSONP}. + * HTTP servers via the browser's [XMLHttpRequest](https://developer.mozilla.org/en/xmlhttprequest) + * object or via [JSONP](http://en.wikipedia.org/wiki/JSONP). * * For unit testing applications that use `$http` service, see * {@link ngMock.$httpBackend $httpBackend mock}. * * For a higher level of abstraction, please check out the {@link ngResource.$resource - * $resource} service. + * $resource} service. * * The $http API is based on the {@link ng.$q deferred/promise APIs} exposed by * the $q service. While for simple usage patterns this doesn't matter much, for advanced usage * it is important to familiarize yourself with these APIs and the guarantees they provide. * * - * # General usage - * The `$http` service is a function which takes a single argument — a configuration object — - * that is used to generate an HTTP request and returns a {@link ng.$q promise} - * with two $http specific methods: `success` and `error`. + * ## General usage + * The `$http` service is a function which takes a single argument — a {@link $http#usage configuration object} — + * that is used to generate an HTTP request and returns a {@link ng.$q promise}. * - *
    -         *   $http({method: 'GET', url: '/someUrl'}).
    -         *     success(function(data, status, headers, config) {
    +         * ```js
    +         *   // Simple GET request example:
    +         *   $http({
    +     *     method: 'GET',
    +     *     url: '/someUrl'
    +     *   }).then(function successCallback(response) {
          *       // this callback will be called asynchronously
          *       // when the response is available
    -     *     }).
    -         *     error(function(data, status, headers, config) {
    +     *     }, function errorCallback(response) {
          *       // called asynchronously if an error occurs
          *       // or server returns response with an error status.
          *     });
    -         * 
    - * - * Since the returned value of calling the $http function is a `promise`, you can also use - * the `then` method to register callbacks, and these callbacks will receive a single argument – - * an object representing the response. See the API signature and type info below for more - * details. - * - * A response status code between 200 and 299 is considered a success status and - * will result in the success callback being called. Note that if the response is a redirect, - * XMLHttpRequest will transparently follow it, meaning that the error callback will not be - * called for such responses. - * - * # Calling $http from outside AngularJS - * The `$http` service will not actually send the request until the next `$digest()` is - * executed. Normally this is not an issue, since almost all the time your call to `$http` will - * be from within a `$apply()` block. - * If you are calling `$http` from outside Angular, then you should wrap it in a call to - * `$apply` to cause a $digest to occur and also to handle errors in the block correctly. - * - * ``` - * $scope.$apply(function() { - * $http(...); - * }); * ``` * - * # Writing Unit Tests that use $http - * When unit testing you are mostly responsible for scheduling the `$digest` cycle. If you do - * not trigger a `$digest` before calling `$httpBackend.flush()` then the request will not have - * been made and `$httpBackend.expect(...)` expectations will fail. The solution is to run the - * code that calls the `$http()` method inside a $apply block as explained in the previous - * section. + * The response object has these properties: * + * - **data** – `{string|Object}` – The response body transformed with the transform + * functions. + * - **status** – `{number}` – HTTP status code of the response. + * - **headers** – `{function([headerName])}` – Header getter function. + * - **config** – `{Object}` – The configuration object that was used to generate the request. + * - **statusText** – `{string}` – HTTP status text of the response. + * - **xhrStatus** – `{string}` – Status of the XMLHttpRequest (`complete`, `error`, `timeout` or `abort`). + * + * A response status code between 200 and 299 is considered a success status and will result in + * the success callback being called. Any response status code outside of that range is + * considered an error status and will result in the error callback being called. + * Also, status codes less than -1 are normalized to zero. -1 usually means the request was + * aborted, e.g. using a `config.timeout`. + * Note that if the response is a redirect, XMLHttpRequest will transparently follow it, meaning + * that the outcome (success or error) will be determined by the final response status code. + * + * + * ## Shortcut methods + * + * Shortcut methods are also available. All shortcut methods require passing in the URL, and + * request data must be passed in for POST/PUT requests. An optional config can be passed as the + * last argument. + * + * ```js + * $http.get('/someUrl', config).then(successCallback, errorCallback); + * $http.post('/someUrl', data, config).then(successCallback, errorCallback); * ``` - * $httpBackend.expectGET(...); - * $scope.$apply(function() { - * $http.get(...); - * }); - * $httpBackend.flush(); - * ``` - * - * # Shortcut methods - * - * Since all invocations of the $http service require passing in an HTTP method and URL, and - * POST/PUT requests require request data to be provided as well, shortcut methods - * were created: - * - *
    -         *   $http.get('/someUrl').success(successCallback);
    -         *   $http.post('/someUrl', data).success(successCallback);
    -         * 
    * * Complete list of shortcut methods: * - * - {@link ng.$http#methods_get $http.get} - * - {@link ng.$http#methods_head $http.head} - * - {@link ng.$http#methods_post $http.post} - * - {@link ng.$http#methods_put $http.put} - * - {@link ng.$http#methods_delete $http.delete} - * - {@link ng.$http#methods_jsonp $http.jsonp} + * - {@link ng.$http#get $http.get} + * - {@link ng.$http#head $http.head} + * - {@link ng.$http#post $http.post} + * - {@link ng.$http#put $http.put} + * - {@link ng.$http#delete $http.delete} + * - {@link ng.$http#jsonp $http.jsonp} + * - {@link ng.$http#patch $http.patch} * * - * # Setting HTTP Headers + * ## Writing Unit Tests that use $http + * When unit testing (using {@link ngMock ngMock}), it is necessary to call + * {@link ngMock.$httpBackend#flush $httpBackend.flush()} to flush each pending + * request using trained responses. + * + * ``` + * $httpBackend.expectGET(...); + * $http.get(...); + * $httpBackend.flush(); + * ``` + * + * ## Setting HTTP Headers * * The $http service will automatically add certain HTTP headers to all requests. These defaults * can be fully configured by accessing the `$httpProvider.defaults.headers` configuration * object, which currently contains this default configuration: * * - `$httpProvider.defaults.headers.common` (headers that are common for all requests): - * - `Accept: application/json, text/plain, * / *` + * - Accept: application/json, text/plain, \*/\* * - `$httpProvider.defaults.headers.post`: (header defaults for POST requests) * - `Content-Type: application/json` * - `$httpProvider.defaults.headers.put` (header defaults for PUT requests) @@ -7122,74 +11685,143 @@ * To add or overwrite these defaults, simply add or remove a property from these configuration * objects. To add headers for an HTTP method other than POST or PUT, simply add a new object * with the lowercased HTTP method name as the key, e.g. - * `$httpProvider.defaults.headers.get = { 'My-Header' : 'value' }. + * `$httpProvider.defaults.headers.get = { 'My-Header' : 'value' }`. * * The defaults can also be set at runtime via the `$http.defaults` object in the same * fashion. For example: * * ``` * module.run(function($http) { - * $http.defaults.headers.common.Authentication = 'Basic YmVlcDpib29w' + * $http.defaults.headers.common.Authorization = 'Basic YmVlcDpib29w'; * }); * ``` * * In addition, you can supply a `headers` property in the config object passed when * calling `$http(config)`, which overrides the defaults without changing them globally. * + * To explicitly remove a header automatically added via $httpProvider.defaults.headers on a per request basis, + * Use the `headers` property, setting the desired header to `undefined`. For example: * - * # Transforming Requests and Responses + * ```js + * var req = { + * method: 'POST', + * url: 'http://example.com', + * headers: { + * 'Content-Type': undefined + * }, + * data: { test: 'test' } + * } * - * Both requests and responses can be transformed using transform functions. By default, Angular - * applies these transformations: + * $http(req).then(function(){...}, function(){...}); + * ``` * - * Request transformations: + * ## Transforming Requests and Responses + * + * Both requests and responses can be transformed using transformation functions: `transformRequest` + * and `transformResponse`. These properties can be a single function that returns + * the transformed value (`function(data, headersGetter, status)`) or an array of such transformation functions, + * which allows you to `push` or `unshift` a new transformation function into the transformation chain. + * + *
    + * **Note:** Angular does not make a copy of the `data` parameter before it is passed into the `transformRequest` pipeline. + * That means changes to the properties of `data` are not local to the transform function (since Javascript passes objects by reference). + * For example, when calling `$http.get(url, $scope.myObject)`, modifications to the object's properties in a transformRequest + * function will be reflected on the scope and in any templates where the object is data-bound. + * To prevent this, transform functions should have no side-effects. + * If you need to modify properties, it is recommended to make a copy of the data, or create new object to return. + *
    + * + * ### Default Transformations + * + * The `$httpProvider` provider and `$http` service expose `defaults.transformRequest` and + * `defaults.transformResponse` properties. If a request does not provide its own transformations + * then these will be applied. + * + * You can augment or replace the default transformations by modifying these properties by adding to or + * replacing the array. + * + * Angular provides the following default transformations: + * + * Request transformations (`$httpProvider.defaults.transformRequest` and `$http.defaults.transformRequest`) is + * an array with one function that does the following: * * - If the `data` property of the request configuration object contains an object, serialize it * into JSON format. * - * Response transformations: + * Response transformations (`$httpProvider.defaults.transformResponse` and `$http.defaults.transformResponse`) is + * an array with one function that does the following: * * - If XSRF prefix is detected, strip it (see Security Considerations section below). - * - If JSON response is detected, deserialize it using a JSON parser. + * - If the `Content-Type` is `application/json` or the response looks like JSON, + * deserialize it using a JSON parser. * - * To globally augment or override the default transforms, modify the - * `$httpProvider.defaults.transformRequest` and `$httpProvider.defaults.transformResponse` - * properties. These properties are by default an array of transform functions, which allows you - * to `push` or `unshift` a new transformation function into the transformation chain. You can - * also decide to completely override any default transformations by assigning your - * transformation functions to these properties directly without the array wrapper. These defaults - * are again available on the $http factory at run-time, which may be useful if you have run-time - * services you wish to be involved in your transformations. * - * Similarly, to locally override the request/response transforms, augment the - * `transformRequest` and/or `transformResponse` properties of the configuration object passed + * ### Overriding the Default Transformations Per Request + * + * If you wish to override the request/response transformations only for a single request then provide + * `transformRequest` and/or `transformResponse` properties on the configuration object passed * into `$http`. * + * Note that if you provide these properties on the config object the default transformations will be + * overwritten. If you wish to augment the default transformations then you must include them in your + * local transformation array. * - * # Caching + * The following code demonstrates adding a new response transformation to be run after the default response + * transformations have been run. * - * To enable caching, set the request configuration `cache` property to `true` (to use default - * cache) or to a custom cache object (built with {@link ng.$cacheFactory `$cacheFactory`}). - * When the cache is enabled, `$http` stores the response from the server in the specified - * cache. The next time the same request is made, the response is served from the cache without - * sending a request to the server. + * ```js + * function appendTransform(defaults, transform) { + * + * // We can't guarantee that the default transformation is an array + * defaults = angular.isArray(defaults) ? defaults : [defaults]; + * + * // Append the new transformation to the defaults + * return defaults.concat(transform); + * } * - * Note that even if the response is served from cache, delivery of the data is asynchronous in - * the same way that real requests are. + * $http({ + * url: '...', + * method: 'GET', + * transformResponse: appendTransform($http.defaults.transformResponse, function(value) { + * return doTransform(value); + * }) + * }); + * ``` * - * If there are multiple GET requests for the same URL that should be cached using the same - * cache, but the cache is not populated yet, only one request to the server will be made and - * the remaining requests will be fulfilled using the response from the first request. * - * You can change the default cache to a new object (built with - * {@link ng.$cacheFactory `$cacheFactory`}) by updating the - * {@link ng.$http#properties_defaults `$http.defaults.cache`} property. All requests who set - * their `cache` property to `true` will now use this cache object. + * ## Caching * - * If you set the default cache to `false` then only requests that specify their own custom - * cache object will be cached. + * {@link ng.$http `$http`} responses are not cached by default. To enable caching, you must + * set the config.cache value or the default cache value to TRUE or to a cache object (created + * with {@link ng.$cacheFactory `$cacheFactory`}). If defined, the value of config.cache takes + * precedence over the default cache value. * - * # Interceptors + * In order to: + * * cache all responses - set the default cache value to TRUE or to a cache object + * * cache a specific response - set config.cache value to TRUE or to a cache object + * + * If caching is enabled, but neither the default cache nor config.cache are set to a cache object, + * then the default `$cacheFactory("$http")` object is used. + * + * The default cache value can be set by updating the + * {@link ng.$http#defaults `$http.defaults.cache`} property or the + * {@link $httpProvider#defaults `$httpProvider.defaults.cache`} property. + * + * When caching is enabled, {@link ng.$http `$http`} stores the response from the server using + * the relevant cache object. The next time the same request is made, the response is returned + * from the cache without sending a request to the server. + * + * Take note that: + * + * * Only GET and JSONP requests are cached. + * * The cache key is the request URL including search parameters; headers are not considered. + * * Cached responses are returned asynchronously, in the same way as responses from the server. + * * If multiple identical requests are made using the same cache, which is not yet populated, + * one request will be made to the server and remaining requests will return the same response. + * * A cache-control header on the response does not affect if or how responses are cached. + * + * + * ## Interceptors * * Before you start creating interceptors, be sure to understand the * {@link ng.$q $q and deferred/promise APIs}. @@ -7199,7 +11831,7 @@ * able to intercept requests before they are handed to the server and * responses before they are handed over to the application code that * initiated these requests. The interceptors leverage the {@link ng.$q - * promise APIs} to fulfill this need for both synchronous and asynchronous pre-processing. + * promise APIs} to fulfill this need for both synchronous and asynchronous pre-processing. * * The interceptors are service factories that are registered with the `$httpProvider` by * adding them to the `$httpProvider.interceptors` array. The factory is called and @@ -7207,26 +11839,26 @@ * * There are two kinds of interceptors (and two kinds of rejection interceptors): * - * * `request`: interceptors get called with http `config` object. The function is free to - * modify the `config` or create a new one. The function needs to return the `config` - * directly or as a promise. + * * `request`: interceptors get called with a http {@link $http#usage config} object. The function is free to + * modify the `config` object or create a new one. The function needs to return the `config` + * object directly, or a promise containing the `config` or a new `config` object. * * `requestError`: interceptor gets called when a previous interceptor threw an error or * resolved with a rejection. * * `response`: interceptors get called with http `response` object. The function is free to - * modify the `response` or create a new one. The function needs to return the `response` - * directly or as a promise. + * modify the `response` object or create a new one. The function needs to return the `response` + * object directly, or as a promise containing the `response` or a new `response` object. * * `responseError`: interceptor gets called when a previous interceptor threw an error or * resolved with a rejection. * * - *
    +         * ```js
              *   // register the interceptor as a service
              *   $provide.factory('myHttpInterceptor', function($q, dependency1, dependency2) {
          *     return {
          *       // optional method
          *       'request': function(config) {
          *         // do something on success
    -     *         return config || $q.when(config);
    +     *         return config;
          *       },
          *
          *       // optional method
    @@ -7243,7 +11875,7 @@
          *       // optional method
          *       'response': function(response) {
          *         // do something on success
    -     *         return response || $q.when(response);
    +     *         return response;
          *       },
          *
          *       // optional method
    @@ -7272,96 +11904,50 @@
          *       }
          *     };
          *   });
    -         * 
    + * ``` * - * # Response interceptors (DEPRECATED) - * - * Before you start creating interceptors, be sure to understand the - * {@link ng.$q $q and deferred/promise APIs}. - * - * For purposes of global error handling, authentication or any kind of synchronous or - * asynchronous preprocessing of received responses, it is desirable to be able to intercept - * responses for http requests before they are handed over to the application code that - * initiated these requests. The response interceptors leverage the {@link ng.$q - * promise apis} to fulfil this need for both synchronous and asynchronous preprocessing. - * - * The interceptors are service factories that are registered with the $httpProvider by - * adding them to the `$httpProvider.responseInterceptors` array. The factory is called and - * injected with dependencies (if specified) and returns the interceptor — a function that - * takes a {@link ng.$q promise} and returns the original or a new promise. - * - *
    -         *   // register the interceptor as a service
    -         *   $provide.factory('myHttpInterceptor', function($q, dependency1, dependency2) {
    -     *     return function(promise) {
    -     *       return promise.then(function(response) {
    -     *         // do something on success
    -     *         return response;
    -     *       }, function(response) {
    -     *         // do something on error
    -     *         if (canRecover(response)) {
    -     *           return responseOrNewPromise
    -     *         }
    -     *         return $q.reject(response);
    -     *       });
    -     *     }
    -     *   });
    -         *
    -         *   $httpProvider.responseInterceptors.push('myHttpInterceptor');
    -         *
    -         *
    -         *   // register the interceptor via an anonymous factory
    -         *   $httpProvider.responseInterceptors.push(function($q, dependency1, dependency2) {
    -     *     return function(promise) {
    -     *       // same as above
    -     *     }
    -     *   });
    -         * 
    - * - * - * # Security Considerations + * ## Security Considerations * * When designing web applications, consider security threats from: * - * - {@link http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx - * JSON vulnerability} - * - {@link http://en.wikipedia.org/wiki/Cross-site_request_forgery XSRF} + * - [JSON vulnerability](http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx) + * - [XSRF](http://en.wikipedia.org/wiki/Cross-site_request_forgery) * * Both server and the client must cooperate in order to eliminate these threats. Angular comes * pre-configured with strategies that address these issues, but for this to work backend server * cooperation is required. * - * ## JSON Vulnerability Protection + * ### JSON Vulnerability Protection * - * A {@link http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx - * JSON vulnerability} allows third party website to turn your JSON resource URL into - * {@link http://en.wikipedia.org/wiki/JSONP JSONP} request under some conditions. To + * A [JSON vulnerability](http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx) + * allows third party website to turn your JSON resource URL into + * [JSONP](http://en.wikipedia.org/wiki/JSONP) request under some conditions. To * counter this your server can prefix all JSON requests with following string `")]}',\n"`. * Angular will automatically strip the prefix before processing it as JSON. * * For example if your server needs to return: - *
    +         * ```js
              * ['one','two']
    -         * 
    + * ``` * * which is vulnerable to attack, your server can return: - *
    +         * ```js
              * )]}',
              * ['one','two']
    -         * 
    + * ``` * * Angular will strip the prefix, before processing the JSON. * * - * ## Cross Site Request Forgery (XSRF) Protection + * ### Cross Site Request Forgery (XSRF) Protection * - * {@link http://en.wikipedia.org/wiki/Cross-site_request_forgery XSRF} is a technique by which - * an unauthorized site can gain your user's private data. Angular provides a mechanism - * to counter XSRF. When performing XHR requests, the $http service reads a token from a cookie - * (by default, `XSRF-TOKEN`) and sets it as an HTTP header (`X-XSRF-TOKEN`). Since only - * JavaScript that runs on your domain could read the cookie, your server can be assured that - * the XHR came from JavaScript running on your domain. The header will not be set for - * cross-domain requests. + * [XSRF](http://en.wikipedia.org/wiki/Cross-site_request_forgery) is an attack technique by + * which the attacker can trick an authenticated user into unknowingly executing actions on your + * website. Angular provides a mechanism to counter XSRF. When performing XHR requests, the + * $http service reads a token from a cookie (by default, `XSRF-TOKEN`) and sets it as an HTTP + * header (`X-XSRF-TOKEN`). Since only JavaScript that runs on your domain could read the + * cookie, your server can be assured that the XHR came from JavaScript running on your domain. + * The header will not be set for cross-domain requests. * * To take advantage of this, your server needs to set a token in a JavaScript readable session * cookie called `XSRF-TOKEN` on the first HTTP GET request. On subsequent XHR requests the @@ -7369,84 +11955,92 @@ * that only JavaScript running on your domain could have sent the request. The token must be * unique for each user and must be verifiable by the server (to prevent the JavaScript from * making up its own tokens). We recommend that the token is a digest of your site's - * authentication cookie with a {@link https://en.wikipedia.org/wiki/Salt_(cryptography) salt} + * authentication cookie with a [salt](https://en.wikipedia.org/wiki/Salt_(cryptography)) * for added security. * * The name of the headers can be specified using the xsrfHeaderName and xsrfCookieName * properties of either $httpProvider.defaults at config-time, $http.defaults at run-time, * or the per-request config object. * + * In order to prevent collisions in environments where multiple Angular apps share the + * same domain or subdomain, we recommend that each application uses unique cookie name. * * @param {object} config Object describing the request to be made and how it should be * processed. The object has following properties: * * - **method** – `{string}` – HTTP method (e.g. 'GET', 'POST', etc) - * - **url** – `{string}` – Absolute or relative URL of the resource that is being requested. - * - **params** – `{Object.}` – Map of strings or objects which will be turned - * to `?key1=value1&key2=value2` after the url. If the value is not a string, it will be - * JSONified. + * - **url** – `{string|TrustedObject}` – Absolute or relative URL of the resource that is being requested; + * or an object created by a call to `$sce.trustAsResourceUrl(url)`. + * - **params** – `{Object.}` – Map of strings or objects which will be serialized + * with the `paramSerializer` and appended as GET parameters. * - **data** – `{string|Object}` – Data to be sent as the request message data. * - **headers** – `{Object}` – Map of strings or functions which return strings representing * HTTP headers to send to the server. If the return value of a function is null, the - * header will not be sent. + * header will not be sent. Functions accept a config object as an argument. + * - **eventHandlers** - `{Object}` - Event listeners to be bound to the XMLHttpRequest object. + * To bind events to the XMLHttpRequest upload object, use `uploadEventHandlers`. + * The handler will be called in the context of a `$apply` block. + * - **uploadEventHandlers** - `{Object}` - Event listeners to be bound to the XMLHttpRequest upload + * object. To bind events to the XMLHttpRequest object, use `eventHandlers`. + * The handler will be called in the context of a `$apply` block. * - **xsrfHeaderName** – `{string}` – Name of HTTP header to populate with the XSRF token. * - **xsrfCookieName** – `{string}` – Name of cookie containing the XSRF token. * - **transformRequest** – * `{function(data, headersGetter)|Array.}` – * transform function or an array of such functions. The transform function takes the http * request body and headers and returns its transformed (typically serialized) version. + * See {@link ng.$http#overriding-the-default-transformations-per-request + * Overriding the Default Transformations} * - **transformResponse** – - * `{function(data, headersGetter)|Array.}` – + * `{function(data, headersGetter, status)|Array.}` – * transform function or an array of such functions. The transform function takes the http - * response body and headers and returns its transformed (typically deserialized) version. - * - **cache** – `{boolean|Cache}` – If true, a default $http cache will be used to cache the - * GET request, otherwise if a cache instance built with - * {@link ng.$cacheFactory $cacheFactory}, this cache will be used for - * caching. + * response body, headers and status and returns its transformed (typically deserialized) version. + * See {@link ng.$http#overriding-the-default-transformations-per-request + * Overriding the Default Transformations} + * - **paramSerializer** - `{string|function(Object):string}` - A function used to + * prepare the string representation of request parameters (specified as an object). + * If specified as string, it is interpreted as function registered with the + * {@link $injector $injector}, which means you can create your own serializer + * by registering it as a {@link auto.$provide#service service}. + * The default serializer is the {@link $httpParamSerializer $httpParamSerializer}; + * alternatively, you can use the {@link $httpParamSerializerJQLike $httpParamSerializerJQLike} + * - **cache** – `{boolean|Object}` – A boolean value or object created with + * {@link ng.$cacheFactory `$cacheFactory`} to enable or disable caching of the HTTP response. + * See {@link $http#caching $http Caching} for more information. * - **timeout** – `{number|Promise}` – timeout in milliseconds, or {@link ng.$q promise} * that should abort the request when resolved. - * - **withCredentials** - `{boolean}` - whether to to set the `withCredentials` flag on the - * XHR object. See {@link https://developer.mozilla.org/en/http_access_control#section_5 - * requests with credentials} for more information. - * - **responseType** - `{string}` - see {@link - * https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#responseType requestType}. + * - **withCredentials** - `{boolean}` - whether to set the `withCredentials` flag on the + * XHR object. See [requests with credentials](https://developer.mozilla.org/docs/Web/HTTP/Access_control_CORS#Requests_with_credentials) + * for more information. + * - **responseType** - `{string}` - see + * [XMLHttpRequest.responseType](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest#xmlhttprequest-responsetype). * - * @returns {HttpPromise} Returns a {@link ng.$q promise} object with the - * standard `then` method and two http specific methods: `success` and `error`. The `then` - * method takes two arguments a success and an error callback which will be called with a - * response object. The `success` and `error` methods take a single argument - a function that - * will be called when the request succeeds or fails respectively. The arguments passed into - * these functions are destructured representation of the response object passed into the - * `then` method. The response object has these properties: + * @returns {HttpPromise} Returns a {@link ng.$q `Promise}` that will be resolved to a response object + * when the request succeeds or fails. * - * - **data** – `{string|Object}` – The response body transformed with the transform - * functions. - * - **status** – `{number}` – HTTP status code of the response. - * - **headers** – `{function([headerName])}` – Header getter function. - * - **config** – `{Object}` – The configuration object that was used to generate the request. * * @property {Array.} pendingRequests Array of config objects for currently pending * requests. This is primarily meant to be used for debugging purposes. * * * @example - + -
    - - -
    - -
    + + -
    http status code: {{status}}
    @@ -7454,84 +12048,187 @@
    - function FetchCtrl($scope, $http, $templateCache) { - $scope.method = 'GET'; - $scope.url = 'http-hello.html'; + angular.module('httpExample', []) + .config(['$sceDelegateProvider', function($sceDelegateProvider) { + // We must whitelist the JSONP endpoint that we are using to show that we trust it + $sceDelegateProvider.resourceUrlWhitelist([ + 'self', + 'https://angularjs.org/**' + ]); + }]) + .controller('FetchController', ['$scope', '$http', '$templateCache', + function($scope, $http, $templateCache) { + $scope.method = 'GET'; + $scope.url = 'http-hello.html'; - $scope.fetch = function() { - $scope.code = null; - $scope.response = null; + $scope.fetch = function() { + $scope.code = null; + $scope.response = null; - $http({method: $scope.method, url: $scope.url, cache: $templateCache}). - success(function(data, status) { - $scope.status = status; - $scope.data = data; - }). - error(function(data, status) { - $scope.data = data || "Request failed"; - $scope.status = status; - }); - }; + $http({method: $scope.method, url: $scope.url, cache: $templateCache}). + then(function(response) { + $scope.status = response.status; + $scope.data = response.data; + }, function(response) { + $scope.data = response.data || 'Request failed'; + $scope.status = response.status; + }); + }; - $scope.updateModel = function(method, url) { - $scope.method = method; - $scope.url = url; - }; - } + $scope.updateModel = function(method, url) { + $scope.method = method; + $scope.url = url; + }; + }]); Hello, $http! - + + var status = element(by.binding('status')); + var data = element(by.binding('data')); + var fetchBtn = element(by.id('fetchbtn')); + var sampleGetBtn = element(by.id('samplegetbtn')); + var invalidJsonpBtn = element(by.id('invalidjsonpbtn')); + it('should make an xhr GET request', function() { - element(':button:contains("Sample GET")').click(); - element(':button:contains("fetch")').click(); - expect(binding('status')).toBe('200'); - expect(binding('data')).toMatch(/Hello, \$http!/); + sampleGetBtn.click(); + fetchBtn.click(); + expect(status.getText()).toMatch('200'); + expect(data.getText()).toMatch(/Hello, \$http!/); }); - it('should make a JSONP request to angularjs.org', function() { - element(':button:contains("Sample JSONP")').click(); - element(':button:contains("fetch")').click(); - expect(binding('status')).toBe('200'); - expect(binding('data')).toMatch(/Super Hero!/); - }); + // Commented out due to flakes. See https://github.com/angular/angular.js/issues/9185 + // it('should make a JSONP request to angularjs.org', function() { +// var sampleJsonpBtn = element(by.id('samplejsonpbtn')); +// sampleJsonpBtn.click(); +// fetchBtn.click(); +// expect(status.getText()).toMatch('200'); +// expect(data.getText()).toMatch(/Super Hero!/); +// }); it('should make JSONP request to invalid URL and invoke the error handler', function() { - element(':button:contains("Invalid JSONP")').click(); - element(':button:contains("fetch")').click(); - expect(binding('status')).toBe('0'); - expect(binding('data')).toBe('Request failed'); + invalidJsonpBtn.click(); + fetchBtn.click(); + expect(status.getText()).toMatch('0'); + expect(data.getText()).toMatch('Request failed'); });
    */ function $http(requestConfig) { - var config = { - transformRequest: defaults.transformRequest, - transformResponse: defaults.transformResponse - }; - var headers = mergeHeaders(requestConfig); - extend(config, requestConfig); - config.headers = headers; - config.method = uppercase(config.method); - - var xsrfValue = urlIsSameOrigin(config.url) - ? $browser.cookies()[config.xsrfCookieName || defaults.xsrfCookieName] - : undefined; - if (xsrfValue) { - headers[(config.xsrfHeaderName || defaults.xsrfHeaderName)] = xsrfValue; + if (!isObject(requestConfig)) { + throw minErr('$http')('badreq', 'Http request configuration must be an object. Received: {0}', requestConfig); } + if (!isString($sce.valueOf(requestConfig.url))) { + throw minErr('$http')('badreq', 'Http request configuration url must be a string or a $sce trusted object. Received: {0}', requestConfig.url); + } - var serverRequest = function(config) { - headers = config.headers; - var reqData = transformData(config.data, headersGetter(headers), config.transformRequest); + var config = extend({ + method: 'get', + transformRequest: defaults.transformRequest, + transformResponse: defaults.transformResponse, + paramSerializer: defaults.paramSerializer, + jsonpCallbackParam: defaults.jsonpCallbackParam + }, requestConfig); + + config.headers = mergeHeaders(requestConfig); + config.method = uppercase(config.method); + config.paramSerializer = isString(config.paramSerializer) ? + $injector.get(config.paramSerializer) : config.paramSerializer; + + $browser.$$incOutstandingRequestCount(); + + var requestInterceptors = []; + var responseInterceptors = []; + var promise = $q.resolve(config); + + // apply interceptors + forEach(reversedInterceptors, function(interceptor) { + if (interceptor.request || interceptor.requestError) { + requestInterceptors.unshift(interceptor.request, interceptor.requestError); + } + if (interceptor.response || interceptor.responseError) { + responseInterceptors.push(interceptor.response, interceptor.responseError); + } + }); + + promise = chainInterceptors(promise, requestInterceptors); + promise = promise.then(serverRequest); + promise = chainInterceptors(promise, responseInterceptors); + promise = promise.finally(completeOutstandingRequest); + + return promise; + + + function chainInterceptors(promise, interceptors) { + for (var i = 0, ii = interceptors.length; i < ii;) { + var thenFn = interceptors[i++]; + var rejectFn = interceptors[i++]; + + promise = promise.then(thenFn, rejectFn); + } + + interceptors.length = 0; + + return promise; + } + + function completeOutstandingRequest() { + $browser.$$completeOutstandingRequest(noop); + } + + function executeHeaderFns(headers, config) { + var headerContent, processedHeaders = {}; + + forEach(headers, function(headerFn, header) { + if (isFunction(headerFn)) { + headerContent = headerFn(config); + if (headerContent != null) { + processedHeaders[header] = headerContent; + } + } else { + processedHeaders[header] = headerFn; + } + }); + + return processedHeaders; + } + + function mergeHeaders(config) { + var defHeaders = defaults.headers, + reqHeaders = extend({}, config.headers), + defHeaderName, lowercaseDefHeaderName, reqHeaderName; + + defHeaders = extend({}, defHeaders.common, defHeaders[lowercase(config.method)]); + + // using for-in instead of forEach to avoid unnecessary iteration after header has been found + defaultHeadersIteration: + for (defHeaderName in defHeaders) { + lowercaseDefHeaderName = lowercase(defHeaderName); + + for (reqHeaderName in reqHeaders) { + if (lowercase(reqHeaderName) === lowercaseDefHeaderName) { + continue defaultHeadersIteration; + } + } + + reqHeaders[defHeaderName] = defHeaders[defHeaderName]; + } + + // execute if header value is a function for merged headers + return executeHeaderFns(reqHeaders, shallowCopy(config)); + } + + function serverRequest(config) { + var headers = config.headers; + var reqData = transformData(config.data, headersGetter(headers), undefined, config.transformRequest); // strip content-type if data is undefined - if (isUndefined(config.data)) { + if (isUndefined(reqData)) { forEach(headers, function(value, header) { if (lowercase(header) === 'content-type') { delete headers[header]; @@ -7544,96 +12241,17 @@ } // send request - return sendReq(config, reqData, headers).then(transformResponse, transformResponse); - }; - - var chain = [serverRequest, undefined]; - var promise = $q.when(config); - - // apply interceptors - forEach(reversedInterceptors, function(interceptor) { - if (interceptor.request || interceptor.requestError) { - chain.unshift(interceptor.request, interceptor.requestError); - } - if (interceptor.response || interceptor.responseError) { - chain.push(interceptor.response, interceptor.responseError); - } - }); - - while(chain.length) { - var thenFn = chain.shift(); - var rejectFn = chain.shift(); - - promise = promise.then(thenFn, rejectFn); + return sendReq(config, reqData).then(transformResponse, transformResponse); } - promise.success = function(fn) { - promise.then(function(response) { - fn(response.data, response.status, response.headers, config); - }); - return promise; - }; - - promise.error = function(fn) { - promise.then(null, function(response) { - fn(response.data, response.status, response.headers, config); - }); - return promise; - }; - - return promise; - function transformResponse(response) { // make a copy since the response must be cacheable - var resp = extend({}, response, { - data: transformData(response.data, response.headers, config.transformResponse) - }); + var resp = extend({}, response); + resp.data = transformData(response.data, response.headers, response.status, + config.transformResponse); return (isSuccess(response.status)) - ? resp - : $q.reject(resp); - } - - function mergeHeaders(config) { - var defHeaders = defaults.headers, - reqHeaders = extend({}, config.headers), - defHeaderName, lowercaseDefHeaderName, reqHeaderName; - - defHeaders = extend({}, defHeaders.common, defHeaders[lowercase(config.method)]); - - // execute if header value is function - execHeaders(defHeaders); - execHeaders(reqHeaders); - - // using for-in instead of forEach to avoid unecessary iteration after header has been found - defaultHeadersIteration: - for (defHeaderName in defHeaders) { - lowercaseDefHeaderName = lowercase(defHeaderName); - - for (reqHeaderName in reqHeaders) { - if (lowercase(reqHeaderName) === lowercaseDefHeaderName) { - continue defaultHeadersIteration; - } - } - - reqHeaders[defHeaderName] = defHeaders[defHeaderName]; - } - - return reqHeaders; - - function execHeaders(headers) { - var headerContent; - - forEach(headers, function(headerFn, header) { - if (isFunction(headerFn)) { - headerContent = headerFn(); - if (headerContent != null) { - headers[header] = headerContent; - } else { - delete headers[header]; - } - } - }); - } + ? resp + : $q.reject(resp); } } @@ -7641,53 +12259,77 @@ /** * @ngdoc method - * @name ng.$http#get - * @methodOf ng.$http + * @name $http#get * * @description * Shortcut method to perform `GET` request. * - * @param {string} url Relative or absolute URL specifying the destination of the request + * @param {string|TrustedObject} url Absolute or relative URL of the resource that is being requested; + * or an object created by a call to `$sce.trustAsResourceUrl(url)`. * @param {Object=} config Optional configuration object * @returns {HttpPromise} Future object */ /** * @ngdoc method - * @name ng.$http#delete - * @methodOf ng.$http + * @name $http#delete * * @description * Shortcut method to perform `DELETE` request. * - * @param {string} url Relative or absolute URL specifying the destination of the request + * @param {string|TrustedObject} url Absolute or relative URL of the resource that is being requested; + * or an object created by a call to `$sce.trustAsResourceUrl(url)`. * @param {Object=} config Optional configuration object * @returns {HttpPromise} Future object */ /** * @ngdoc method - * @name ng.$http#head - * @methodOf ng.$http + * @name $http#head * * @description * Shortcut method to perform `HEAD` request. * - * @param {string} url Relative or absolute URL specifying the destination of the request + * @param {string|TrustedObject} url Absolute or relative URL of the resource that is being requested; + * or an object created by a call to `$sce.trustAsResourceUrl(url)`. * @param {Object=} config Optional configuration object * @returns {HttpPromise} Future object */ /** * @ngdoc method - * @name ng.$http#jsonp - * @methodOf ng.$http + * @name $http#jsonp * * @description * Shortcut method to perform `JSONP` request. * - * @param {string} url Relative or absolute URL specifying the destination of the request. - * Should contain `JSON_CALLBACK` string. + * Note that, since JSONP requests are sensitive because the response is given full access to the browser, + * the url must be declared, via {@link $sce} as a trusted resource URL. + * You can trust a URL by adding it to the whitelist via + * {@link $sceDelegateProvider#resourceUrlWhitelist `$sceDelegateProvider.resourceUrlWhitelist`} or + * by explicitly trusting the URL via {@link $sce#trustAsResourceUrl `$sce.trustAsResourceUrl(url)`}. + * + * JSONP requests must specify a callback to be used in the response from the server. This callback + * is passed as a query parameter in the request. You must specify the name of this parameter by + * setting the `jsonpCallbackParam` property on the request config object. + * + * ``` + * $http.jsonp('some/trusted/url', {jsonpCallbackParam: 'callback'}) + * ``` + * + * You can also specify a default callback parameter name in `$http.defaults.jsonpCallbackParam`. + * Initially this is set to `'callback'`. + * + *
    + * You can no longer use the `JSON_CALLBACK` string as a placeholder for specifying where the callback + * parameter value should go. + *
    + * + * If you would like to customise where and how the callbacks are stored then try overriding + * or decorating the {@link $jsonpCallbacks} service. + * + * @param {string|TrustedObject} url Absolute or relative URL of the resource that is being requested; + * or an object created by a call to `$sce.trustAsResourceUrl(url)`. * @param {Object=} config Optional configuration object * @returns {HttpPromise} Future object */ @@ -7695,8 +12337,7 @@ /** * @ngdoc method - * @name ng.$http#post - * @methodOf ng.$http + * @name $http#post * * @description * Shortcut method to perform `POST` request. @@ -7709,8 +12350,7 @@ /** * @ngdoc method - * @name ng.$http#put - * @methodOf ng.$http + * @name $http#put * * @description * Shortcut method to perform `PUT` request. @@ -7720,12 +12360,24 @@ * @param {Object=} config Optional configuration object * @returns {HttpPromise} Future object */ - createShortMethodsWithData('post', 'put'); + + /** + * @ngdoc method + * @name $http#patch + * + * @description + * Shortcut method to perform `PATCH` request. + * + * @param {string} url Relative or absolute URL specifying the destination of the request + * @param {*} data Request content + * @param {Object=} config Optional configuration object + * @returns {HttpPromise} Future object + */ + createShortMethodsWithData('post', 'put', 'patch'); /** * @ngdoc property - * @name ng.$http#defaults - * @propertyOf ng.$http + * @name $http#defaults * * @description * Runtime equivalent of the `$httpProvider.defaults` property. Allows configuration of @@ -7742,7 +12394,7 @@ function createShortMethods(names) { forEach(arguments, function(name) { $http[name] = function(url, config) { - return $http(extend(config || {}, { + return $http(extend({}, config || {}, { method: name, url: url })); @@ -7754,7 +12406,7 @@ function createShortMethodsWithData(name) { forEach(arguments, function(name) { $http[name] = function(url, data, config) { - return $http(extend(config || {}, { + return $http(extend({}, config || {}, { method: name, url: url, data: data @@ -7770,36 +12422,54 @@ * !!! ACCESSES CLOSURE VARS: * $httpBackend, defaults, $log, $rootScope, defaultCache, $http.pendingRequests */ - function sendReq(config, reqData, reqHeaders) { + function sendReq(config, reqData) { var deferred = $q.defer(), - promise = deferred.promise, - cache, - cachedResp, - url = buildUrl(config.url, config.params); + promise = deferred.promise, + cache, + cachedResp, + reqHeaders = config.headers, + isJsonp = lowercase(config.method) === 'jsonp', + url = config.url; + + if (isJsonp) { + // JSONP is a pretty sensitive operation where we're allowing a script to have full access to + // our DOM and JS space. So we require that the URL satisfies SCE.RESOURCE_URL. + url = $sce.getTrustedResourceUrl(url); + } else if (!isString(url)) { + // If it is not a string then the URL must be a $sce trusted object + url = $sce.valueOf(url); + } + + url = buildUrl(url, config.paramSerializer(config.params)); + + if (isJsonp) { + // Check the url and add the JSONP callback placeholder + url = sanitizeJsonpCallbackParam(url, config.jsonpCallbackParam); + } $http.pendingRequests.push(config); promise.then(removePendingReq, removePendingReq); - - if ((config.cache || defaults.cache) && config.cache !== false && config.method == 'GET') { + if ((config.cache || defaults.cache) && config.cache !== false && + (config.method === 'GET' || config.method === 'JSONP')) { cache = isObject(config.cache) ? config.cache - : isObject(defaults.cache) ? defaults.cache + : isObject(/** @type {?} */ (defaults).cache) + ? /** @type {?} */ (defaults).cache : defaultCache; } if (cache) { cachedResp = cache.get(url); if (isDefined(cachedResp)) { - if (cachedResp.then) { + if (isPromiseLike(cachedResp)) { // cached request has already been sent, but there is no response yet - cachedResp.then(removePendingReq, removePendingReq); - return cachedResp; + cachedResp.then(resolvePromiseWithResult, resolvePromiseWithResult); } else { // serving from cache if (isArray(cachedResp)) { - resolvePromise(cachedResp[1], cachedResp[0], copy(cachedResp[2])); + resolvePromise(cachedResp[1], cachedResp[0], shallowCopy(cachedResp[2]), cachedResp[3], cachedResp[4]); } else { - resolvePromise(cachedResp, 200, {}); + resolvePromise(cachedResp, 200, {}, 'OK', 'complete'); } } } else { @@ -7808,14 +12478,47 @@ } } - // if we won't have the response in cache, send the request to the backend + + // if we won't have the response in cache, set the xsrf headers and + // send the request to the backend if (isUndefined(cachedResp)) { + var xsrfValue = urlIsSameOrigin(config.url) + ? $$cookieReader()[config.xsrfCookieName || defaults.xsrfCookieName] + : undefined; + if (xsrfValue) { + reqHeaders[(config.xsrfHeaderName || defaults.xsrfHeaderName)] = xsrfValue; + } + $httpBackend(config.method, url, reqData, done, reqHeaders, config.timeout, - config.withCredentials, config.responseType); + config.withCredentials, config.responseType, + createApplyHandlers(config.eventHandlers), + createApplyHandlers(config.uploadEventHandlers)); } return promise; + function createApplyHandlers(eventHandlers) { + if (eventHandlers) { + var applyHandlers = {}; + forEach(eventHandlers, function(eventHandler, key) { + applyHandlers[key] = function(event) { + if (useApplyAsync) { + $rootScope.$applyAsync(callEventHandler); + } else if ($rootScope.$$phase) { + callEventHandler(); + } else { + $rootScope.$apply(callEventHandler); + } + + function callEventHandler() { + eventHandler(event); + } + }; + }); + return applyHandlers; + } + } + /** * Callback registered to $httpBackend(): @@ -7823,81 +12526,121 @@ * - resolves the raw $http promise * - calls $apply */ - function done(status, response, headersString) { + function done(status, response, headersString, statusText, xhrStatus) { if (cache) { if (isSuccess(status)) { - cache.put(url, [status, response, parseHeaders(headersString)]); + cache.put(url, [status, response, parseHeaders(headersString), statusText, xhrStatus]); } else { // remove promise from the cache cache.remove(url); } } - resolvePromise(response, status, headersString); - if (!$rootScope.$$phase) $rootScope.$apply(); + function resolveHttpPromise() { + resolvePromise(response, status, headersString, statusText, xhrStatus); + } + + if (useApplyAsync) { + $rootScope.$applyAsync(resolveHttpPromise); + } else { + resolveHttpPromise(); + if (!$rootScope.$$phase) $rootScope.$apply(); + } } /** * Resolves the raw $http promise. */ - function resolvePromise(response, status, headers) { - // normalize internal statuses to 0 - status = Math.max(status, 0); + function resolvePromise(response, status, headers, statusText, xhrStatus) { + //status: HTTP response status code, 0, -1 (aborted by timeout / promise) + status = status >= -1 ? status : 0; (isSuccess(status) ? deferred.resolve : deferred.reject)({ data: response, status: status, headers: headersGetter(headers), - config: config + config: config, + statusText: statusText, + xhrStatus: xhrStatus }); } + function resolvePromiseWithResult(result) { + resolvePromise(result.data, result.status, shallowCopy(result.headers()), result.statusText, result.xhrStatus); + } function removePendingReq() { - var idx = indexOf($http.pendingRequests, config); + var idx = $http.pendingRequests.indexOf(config); if (idx !== -1) $http.pendingRequests.splice(idx, 1); } } - function buildUrl(url, params) { - if (!params) return url; - var parts = []; - forEachSorted(params, function(value, key) { - if (value === null || isUndefined(value)) return; - if (!isArray(value)) value = [value]; - - forEach(value, function(v) { - if (isObject(v)) { - v = toJson(v); - } - parts.push(encodeUriQuery(key) + '=' + - encodeUriQuery(v)); - }); - }); - return url + ((url.indexOf('?') == -1) ? '?' : '&') + parts.join('&'); + function buildUrl(url, serializedParams) { + if (serializedParams.length > 0) { + url += ((url.indexOf('?') === -1) ? '?' : '&') + serializedParams; + } + return url; } + function sanitizeJsonpCallbackParam(url, key) { + if (/[&?][^=]+=JSON_CALLBACK/.test(url)) { + // Throw if the url already contains a reference to JSON_CALLBACK + throw $httpMinErr('badjsonp', 'Illegal use of JSON_CALLBACK in url, "{0}"', url); + } + var callbackParamRegex = new RegExp('[&?]' + key + '='); + if (callbackParamRegex.test(url)) { + // Throw if the callback param was already provided + throw $httpMinErr('badjsonp', 'Illegal use of callback param, "{0}", in url, "{1}"', key, url); + } + + // Add in the JSON_CALLBACK callback param value + url += ((url.indexOf('?') === -1) ? '?' : '&') + key + '=JSON_CALLBACK'; + + return url; + } }]; } - function createXhr(method) { - // IE8 doesn't support PATCH method, but the ActiveX object does - /* global ActiveXObject */ - return (msie <= 8 && lowercase(method) === 'patch') - ? new ActiveXObject('Microsoft.XMLHTTP') - : new window.XMLHttpRequest(); + /** + * @ngdoc service + * @name $xhrFactory + * @this + * + * @description + * Factory function used to create XMLHttpRequest objects. + * + * Replace or decorate this service to create your own custom XMLHttpRequest objects. + * + * ``` + * angular.module('myApp', []) + * .factory('$xhrFactory', function() { + * return function createXhr(method, url) { + * return new window.XMLHttpRequest({mozSystem: true}); + * }; + * }); + * ``` + * + * @param {string} method HTTP method of the request (GET, POST, PUT, ..) + * @param {string} url URL of the request. + */ + function $xhrFactoryProvider() { + this.$get = function() { + return function createXhr() { + return new window.XMLHttpRequest(); + }; + }; } - /** - * @ngdoc object - * @name ng.$httpBackend - * @requires $browser - * @requires $window + * @ngdoc service + * @name $httpBackend + * @requires $jsonpCallbacks * @requires $document + * @requires $xhrFactory + * @this * * @description * HTTP backend used by the {@link ng.$http service} that delegates to @@ -7907,41 +12650,30 @@ * {@link ng.$http $http} or {@link ngResource.$resource $resource}. * * During testing this implementation is swapped with {@link ngMock.$httpBackend mock - * $httpBackend} which can be trained with responses. + * $httpBackend} which can be trained with responses. */ function $HttpBackendProvider() { - this.$get = ['$browser', '$window', '$document', function($browser, $window, $document) { - return createHttpBackend($browser, createXhr, $browser.defer, $window.angular.callbacks, $document[0]); + this.$get = ['$browser', '$jsonpCallbacks', '$document', '$xhrFactory', function($browser, $jsonpCallbacks, $document, $xhrFactory) { + return createHttpBackend($browser, $xhrFactory, $browser.defer, $jsonpCallbacks, $document[0]); }]; } function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDocument) { - var ABORTED = -1; - // TODO(vojta): fix the signature - return function(method, url, post, callback, headers, timeout, withCredentials, responseType) { - var status; - $browser.$$incOutstandingRequestCount(); + return function(method, url, post, callback, headers, timeout, withCredentials, responseType, eventHandlers, uploadEventHandlers) { url = url || $browser.url(); - if (lowercase(method) == 'jsonp') { - var callbackId = '_' + (callbacks.counter++).toString(36); - callbacks[callbackId] = function(data) { - callbacks[callbackId].data = data; - }; - - var jsonpDone = jsonpReq(url.replace('JSON_CALLBACK', 'angular.callbacks.' + callbackId), - function() { - if (callbacks[callbackId].data) { - completeRequest(callback, 200, callbacks[callbackId].data); - } else { - completeRequest(callback, status || -2); - } - callbacks[callbackId] = angular.noop; - }); + if (lowercase(method) === 'jsonp') { + var callbackPath = callbacks.createCallback(url); + var jsonpDone = jsonpReq(url, callbackPath, function(status, text) { + // jsonpReq only ever sets status to 200 (OK), 404 (ERROR) or -1 (WAITING) + var response = (status === 200) && callbacks.getResponse(callbackPath); + completeRequest(callback, status, response, '', text, 'complete'); + callbacks.removeCallback(callbackPath); + }); } else { - var xhr = createXhr(method); + var xhr = createXhr(method, url); xhr.open(method, url, true); forEach(headers, function(value, key) { @@ -7950,123 +12682,180 @@ } }); - // In IE6 and 7, this might be called synchronously when xhr.send below is called and the - // response is in the cache. the promise api will ensure that to the app code the api is - // always async - xhr.onreadystatechange = function() { - // onreadystatechange might get called multiple times with readyState === 4 on mobile webkit caused by - // xhrs that are resolved while the app is in the background (see #5426). - // since calling completeRequest sets the `xhr` variable to null, we just check if it's not null before - // continuing - // - // we can't set xhr.onreadystatechange to undefined or delete it because that breaks IE8 (method=PATCH) and - // Safari respectively. - if (xhr && xhr.readyState == 4) { - var responseHeaders = null, - response = null; + xhr.onload = function requestLoaded() { + var statusText = xhr.statusText || ''; - if(status !== ABORTED) { - responseHeaders = xhr.getAllResponseHeaders(); + // responseText is the old-school way of retrieving response (supported by IE9) + // response/responseType properties were introduced in XHR Level2 spec (supported by IE10) + var response = ('response' in xhr) ? xhr.response : xhr.responseText; - // responseText is the old-school way of retrieving response (supported by IE8 & 9) - // response/responseType properties were introduced in XHR Level2 spec (supported by IE10) - response = ('response' in xhr) ? xhr.response : xhr.responseText; - } + // normalize IE9 bug (http://bugs.jquery.com/ticket/1450) + var status = xhr.status === 1223 ? 204 : xhr.status; - completeRequest(callback, - status || xhr.status, - response, - responseHeaders); + // fix status code when it is 0 (0 status is undocumented). + // Occurs when accessing file resources or on Android 4.1 stock browser + // while retrieving files from application cache. + if (status === 0) { + status = response ? 200 : urlResolve(url).protocol === 'file' ? 404 : 0; } + + completeRequest(callback, + status, + response, + xhr.getAllResponseHeaders(), + statusText, + 'complete'); }; + var requestError = function() { + // The response is always empty + // See https://xhr.spec.whatwg.org/#request-error-steps and https://fetch.spec.whatwg.org/#concept-network-error + completeRequest(callback, -1, null, null, '', 'error'); + }; + + var requestAborted = function() { + completeRequest(callback, -1, null, null, '', 'abort'); + }; + + var requestTimeout = function() { + // The response is always empty + // See https://xhr.spec.whatwg.org/#request-error-steps and https://fetch.spec.whatwg.org/#concept-network-error + completeRequest(callback, -1, null, null, '', 'timeout'); + }; + + xhr.onerror = requestError; + xhr.onabort = requestAborted; + xhr.ontimeout = requestTimeout; + + forEach(eventHandlers, function(value, key) { + xhr.addEventListener(key, value); + }); + + forEach(uploadEventHandlers, function(value, key) { + xhr.upload.addEventListener(key, value); + }); + if (withCredentials) { xhr.withCredentials = true; } if (responseType) { - xhr.responseType = responseType; + try { + xhr.responseType = responseType; + } catch (e) { + // WebKit added support for the json responseType value on 09/03/2013 + // https://bugs.webkit.org/show_bug.cgi?id=73648. Versions of Safari prior to 7 are + // known to throw when setting the value "json" as the response type. Other older + // browsers implementing the responseType + // + // The json response type can be ignored if not supported, because JSON payloads are + // parsed on the client-side regardless. + if (responseType !== 'json') { + throw e; + } + } } - xhr.send(post || null); + xhr.send(isUndefined(post) ? null : post); } if (timeout > 0) { var timeoutId = $browserDefer(timeoutRequest, timeout); - } else if (timeout && timeout.then) { + } else if (isPromiseLike(timeout)) { timeout.then(timeoutRequest); } function timeoutRequest() { - status = ABORTED; - jsonpDone && jsonpDone(); - xhr && xhr.abort(); + if (jsonpDone) { + jsonpDone(); + } + if (xhr) { + xhr.abort(); + } } - function completeRequest(callback, status, response, headersString) { + function completeRequest(callback, status, response, headersString, statusText, xhrStatus) { // cancel timeout and subsequent timeout promise resolution - timeoutId && $browserDefer.cancel(timeoutId); + if (isDefined(timeoutId)) { + $browserDefer.cancel(timeoutId); + } jsonpDone = xhr = null; - // fix status code when it is 0 (0 status is undocumented). - // Occurs when accessing file resources. - // On Android 4.1 stock browser it occurs while retrieving files from application cache. - status = (status === 0) ? (response ? 200 : 404) : status; - - // normalize IE bug (http://bugs.jquery.com/ticket/1450) - status = status == 1223 ? 204 : status; - - callback(status, response, headersString); - $browser.$$completeOutstandingRequest(noop); + callback(status, response, headersString, statusText, xhrStatus); } }; - function jsonpReq(url, done) { - // we can't use jQuery/jqLite here because jQuery does crazy shit with script elements, e.g.: + function jsonpReq(url, callbackPath, done) { + url = url.replace('JSON_CALLBACK', callbackPath); + // we can't use jQuery/jqLite here because jQuery does crazy stuff with script elements, e.g.: // - fetches local scripts via XHR and evals them // - adds and immediately removes script elements from the document - var script = rawDocument.createElement('script'), - doneWrapper = function() { - script.onreadystatechange = script.onload = script.onerror = null; - rawDocument.body.removeChild(script); - if (done) done(); - }; - + var script = rawDocument.createElement('script'), callback = null; script.type = 'text/javascript'; script.src = url; + script.async = true; - if (msie && msie <= 8) { - script.onreadystatechange = function() { - if (/loaded|complete/.test(script.readyState)) { - doneWrapper(); + callback = function(event) { + script.removeEventListener('load', callback); + script.removeEventListener('error', callback); + rawDocument.body.removeChild(script); + script = null; + var status = -1; + var text = 'unknown'; + + if (event) { + if (event.type === 'load' && !callbacks.wasCalled(callbackPath)) { + event = { type: 'error' }; } - }; - } else { - script.onload = script.onerror = function() { - doneWrapper(); - }; - } + text = event.type; + status = event.type === 'error' ? 404 : 200; + } + if (done) { + done(status, text); + } + }; + + script.addEventListener('load', callback); + script.addEventListener('error', callback); rawDocument.body.appendChild(script); - return doneWrapper; + return callback; } } - var $interpolateMinErr = minErr('$interpolate'); + var $interpolateMinErr = angular.$interpolateMinErr = minErr('$interpolate'); + $interpolateMinErr.throwNoconcat = function(text) { + throw $interpolateMinErr('noconcat', + 'Error while interpolating: {0}\nStrict Contextual Escaping disallows ' + + 'interpolations that concatenate multiple expressions when a trusted value is ' + + 'required. See http://docs.angularjs.org/api/ng.$sce', text); + }; + + $interpolateMinErr.interr = function(text, err) { + return $interpolateMinErr('interr', 'Can\'t interpolate: {0}\n{1}', text, err.toString()); + }; /** - * @ngdoc object - * @name ng.$interpolateProvider - * @function + * @ngdoc provider + * @name $interpolateProvider + * @this * * @description * * Used for configuring the interpolation markup. Defaults to `{{` and `}}`. * + *
    + * This feature is sometimes used to mix different markup languages, e.g. to wrap an Angular + * template within a Python Jinja template (or any other template language). Mixing templating + * languages is **very dangerous**. The embedding template language will not safely escape Angular + * expressions, so any user-controlled values in the template will cause Cross Site Scripting (XSS) + * security bugs! + *
    + * * @example - - + + -
    +
    //demo.label//
    - - + + it('should interpolate binding with custom symbols', function() { - expect(binding('demo.label')).toBe('This binding is brought you by // interpolation symbols.'); - }); - - + expect(element(by.binding('demo.label')).getText()).toBe('This binding is brought you by // interpolation symbols.'); + }); + + */ function $InterpolateProvider() { var startSymbol = '{{'; @@ -8097,15 +12886,14 @@ /** * @ngdoc method - * @name ng.$interpolateProvider#startSymbol - * @methodOf ng.$interpolateProvider + * @name $interpolateProvider#startSymbol * @description * Symbol to denote start of expression in the interpolated string. Defaults to `{{`. * - * @param {string=} value new value to set the starting symbol to. + * @param {string=} value new value to set the starting symbol to. * @returns {string|self} Returns the symbol when used as getter and self if used as setter. */ - this.startSymbol = function(value){ + this.startSymbol = function(value) { if (value) { startSymbol = value; return this; @@ -8116,15 +12904,14 @@ /** * @ngdoc method - * @name ng.$interpolateProvider#endSymbol - * @methodOf ng.$interpolateProvider + * @name $interpolateProvider#endSymbol * @description * Symbol to denote the end of expression in the interpolated string. Defaults to `}}`. * * @param {string=} value new value to set the ending symbol to. * @returns {string|self} Returns the symbol when used as getter and self if used as setter. */ - this.endSymbol = function(value){ + this.endSymbol = function(value) { if (value) { endSymbol = value; return this; @@ -8136,12 +12923,32 @@ this.$get = ['$parse', '$exceptionHandler', '$sce', function($parse, $exceptionHandler, $sce) { var startSymbolLength = startSymbol.length, - endSymbolLength = endSymbol.length; + endSymbolLength = endSymbol.length, + escapedStartRegexp = new RegExp(startSymbol.replace(/./g, escape), 'g'), + escapedEndRegexp = new RegExp(endSymbol.replace(/./g, escape), 'g'); + + function escape(ch) { + return '\\\\\\' + ch; + } + + function unescapeText(text) { + return text.replace(escapedStartRegexp, startSymbol). + replace(escapedEndRegexp, endSymbol); + } + + // TODO: this is the same as the constantWatchDelegate in parse.js + function constantWatchDelegate(scope, listener, objectEquality, constantInterp) { + var unwatch = scope.$watch(function constantInterpolateWatch(scope) { + unwatch(); + return constantInterp(scope); + }, listener, objectEquality); + return unwatch; + } /** - * @ngdoc function - * @name ng.$interpolate - * @function + * @ngdoc service + * @name $interpolate + * @kind function * * @requires $parse * @requires $sce @@ -8154,58 +12961,152 @@ * interpolation markup. * * -
    -       var $interpolate = ...; // injected
    -       var exp = $interpolate('Hello {{name | uppercase}}!');
    -       expect(exp({name:'Angular'}).toEqual('Hello ANGULAR!');
    -       
    + * ```js + * var $interpolate = ...; // injected + * var exp = $interpolate('Hello {{name | uppercase}}!'); + * expect(exp({name:'Angular'})).toEqual('Hello ANGULAR!'); + * ``` * + * `$interpolate` takes an optional fourth argument, `allOrNothing`. If `allOrNothing` is + * `true`, the interpolation function will return `undefined` unless all embedded expressions + * evaluate to a value other than `undefined`. + * + * ```js + * var $interpolate = ...; // injected + * var context = {greeting: 'Hello', name: undefined }; + * + * // default "forgiving" mode + * var exp = $interpolate('{{greeting}} {{name}}!'); + * expect(exp(context)).toEqual('Hello !'); + * + * // "allOrNothing" mode + * exp = $interpolate('{{greeting}} {{name}}!', false, null, true); + * expect(exp(context)).toBeUndefined(); + * context.name = 'Angular'; + * expect(exp(context)).toEqual('Hello Angular!'); + * ``` + * + * `allOrNothing` is useful for interpolating URLs. `ngSrc` and `ngSrcset` use this behavior. + * + * #### Escaped Interpolation + * $interpolate provides a mechanism for escaping interpolation markers. Start and end markers + * can be escaped by preceding each of their characters with a REVERSE SOLIDUS U+005C (backslash). + * It will be rendered as a regular start/end marker, and will not be interpreted as an expression + * or binding. + * + * This enables web-servers to prevent script injection attacks and defacing attacks, to some + * degree, while also enabling code examples to work without relying on the + * {@link ng.directive:ngNonBindable ngNonBindable} directive. + * + * **For security purposes, it is strongly encouraged that web servers escape user-supplied data, + * replacing angle brackets (<, >) with &lt; and &gt; respectively, and replacing all + * interpolation start/end markers with their escaped counterparts.** + * + * Escaped interpolation markers are only replaced with the actual interpolation markers in rendered + * output when the $interpolate service processes the text. So, for HTML elements interpolated + * by {@link ng.$compile $compile}, or otherwise interpolated with the `mustHaveExpression` parameter + * set to `true`, the interpolated text must contain an unescaped interpolation expression. As such, + * this is typically useful only when user-data is used in rendering a template from the server, or + * when otherwise untrusted data is used by a directive. + * + * + * + *
    + *

    {{apptitle}}: \{\{ username = "defaced value"; \}\} + *

    + *

    {{username}} attempts to inject code which will deface the + * application, but fails to accomplish their task, because the server has correctly + * escaped the interpolation start/end markers with REVERSE SOLIDUS U+005C (backslash) + * characters.

    + *

    Instead, the result of the attempted script injection is visible, and can be removed + * from the database by an administrator.

    + *
    + *
    + *
    + * + * @knownIssue + * It is currently not possible for an interpolated expression to contain the interpolation end + * symbol. For example, `{{ '}}' }}` will be incorrectly interpreted as `{{ ' }}` + `' }}`, i.e. + * an interpolated expression consisting of a single-quote (`'`) and the `' }}` string. + * + * @knownIssue + * All directives and components must use the standard `{{` `}}` interpolation symbols + * in their templates. If you change the application interpolation symbols the {@link $compile} + * service will attempt to denormalize the standard symbols to the custom symbols. + * The denormalization process is not clever enough to know not to replace instances of the standard + * symbols where they would not normally be treated as interpolation symbols. For example in the following + * code snippet the closing braces of the literal object will get incorrectly denormalized: + * + * ``` + *
    + * ``` + * + * See https://github.com/angular/angular.js/pull/14610#issuecomment-219401099 for more information. * * @param {string} text The text with markup to interpolate. * @param {boolean=} mustHaveExpression if set to true then the interpolation string must have * embedded expression in order to return an interpolation function. Strings with no * embedded expression will return null for the interpolation function. * @param {string=} trustedContext when provided, the returned function passes the interpolated - * result through {@link ng.$sce#methods_getTrusted $sce.getTrusted(interpolatedResult, - * trustedContext)} before returning it. Refer to the {@link ng.$sce $sce} service that + * result through {@link ng.$sce#getTrusted $sce.getTrusted(interpolatedResult, + * trustedContext)} before returning it. Refer to the {@link ng.$sce $sce} service that * provides Strict Contextual Escaping for details. + * @param {boolean=} allOrNothing if `true`, then the returned function returns undefined + * unless all embedded expressions evaluate to a value other than `undefined`. * @returns {function(context)} an interpolation function which is used to compute the * interpolated string. The function has these parameters: * - * * `context`: an object against which any expressions embedded in the strings are evaluated - * against. - * + * - `context`: evaluation context for all expressions embedded in the interpolated text */ - function $interpolate(text, mustHaveExpression, trustedContext) { - var startIndex, - endIndex, - index = 0, - parts = [], - length = text.length, - hasInterpolation = false, - fn, - exp, - concat = []; - - while(index < length) { - if ( ((startIndex = text.indexOf(startSymbol, index)) != -1) && - ((endIndex = text.indexOf(endSymbol, startIndex + startSymbolLength)) != -1) ) { - (index != startIndex) && parts.push(text.substring(index, startIndex)); - parts.push(fn = $parse(exp = text.substring(startIndex + startSymbolLength, endIndex))); - fn.exp = exp; - index = endIndex + endSymbolLength; - hasInterpolation = true; - } else { - // we did not find anything, so we have to add the remainder to the parts array - (index != length) && parts.push(text.substring(index)); - index = length; + function $interpolate(text, mustHaveExpression, trustedContext, allOrNothing) { + // Provide a quick exit and simplified result function for text with no interpolation + if (!text.length || text.indexOf(startSymbol) === -1) { + var constantInterp; + if (!mustHaveExpression) { + var unescapedText = unescapeText(text); + constantInterp = valueFn(unescapedText); + constantInterp.exp = text; + constantInterp.expressions = []; + constantInterp.$$watchDelegate = constantWatchDelegate; } + return constantInterp; } - if (!(length = parts.length)) { - // we added, nothing, must have been an empty string. - parts.push(''); - length = 1; + allOrNothing = !!allOrNothing; + var startIndex, + endIndex, + index = 0, + expressions = [], + parseFns = [], + textLength = text.length, + exp, + concat = [], + expressionPositions = []; + + while (index < textLength) { + if (((startIndex = text.indexOf(startSymbol, index)) !== -1) && + ((endIndex = text.indexOf(endSymbol, startIndex + startSymbolLength)) !== -1)) { + if (index !== startIndex) { + concat.push(unescapeText(text.substring(index, startIndex))); + } + exp = text.substring(startIndex + startSymbolLength, endIndex); + expressions.push(exp); + parseFns.push($parse(exp, parseStringifyInterceptor)); + index = endIndex + endSymbolLength; + expressionPositions.push(concat.length); + concat.push(''); + } else { + // we did not find an interpolation, so we have to add the remainder to the separators array + if (index !== textLength) { + concat.push(unescapeText(text.substring(index))); + } + break; + } } // Concatenating expressions makes it hard to reason about whether some combination of @@ -8214,58 +13115,77 @@ // that's used is assigned or constructed by some JS code somewhere that is more testable or // make it obvious that you bound the value to some user controlled value. This helps reduce // the load when auditing for XSS issues. - if (trustedContext && parts.length > 1) { - throw $interpolateMinErr('noconcat', - "Error while interpolating: {0}\nStrict Contextual Escaping disallows " + - "interpolations that concatenate multiple expressions when a trusted value is " + - "required. See http://docs.angularjs.org/../api/ng.$sce", text); + if (trustedContext && concat.length > 1) { + $interpolateMinErr.throwNoconcat(text); } - if (!mustHaveExpression || hasInterpolation) { - concat.length = length; - fn = function(context) { - try { - for(var i = 0, ii = length, part; i * - * @param {function()} fn A function that should be called repeatedly. + * @param {function()} fn A function that should be called repeatedly. If no additional arguments + * are passed (see below), the function is called with the current iteration count. * @param {number} delay Number of milliseconds between each function call. * @param {number=} [count=0] Number of times to repeat. If not set, or 0, will repeat * indefinitely. * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise - * will invoke `fn` within the {@link ng.$rootScope.Scope#methods_$apply $apply} block. - * @returns {promise} A promise which will be notified on each iteration. + * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block. + * @param {...*=} Pass additional parameters to the executed function. + * @returns {promise} A promise which will be notified on each iteration. It will resolve once all iterations of the interval complete. * * @example - - - - -
    -
    - Date format:
    - Current time is: -
    - Blood 1 : {{blood_1}} - Blood 2 : {{blood_2}} - - - -
    -
    - -
    -
    + * + * + * + * + *
    + *
    + *
    + * Current time is: + *
    + * Blood 1 : {{blood_1}} + * Blood 2 : {{blood_2}} + * + * + * + *
    + *
    + * + *
    + *
    */ function interval(fn, delay, count, invokeApply) { - var setInterval = $window.setInterval, - clearInterval = $window.clearInterval, - deferred = $q.defer(), - promise = deferred.promise, - iteration = 0, - skipApply = (isDefined(invokeApply) && !invokeApply); + var hasParams = arguments.length > 4, + args = hasParams ? sliceArgs(arguments, 4) : [], + setInterval = $window.setInterval, + clearInterval = $window.clearInterval, + iteration = 0, + skipApply = (isDefined(invokeApply) && !invokeApply), + deferred = (skipApply ? $$q : $q).defer(), + promise = deferred.promise; - count = isDefined(count) ? count : 0, - - promise.then(null, null, fn); + count = isDefined(count) ? count : 0; promise.$$intervalId = setInterval(function tick() { + if (skipApply) { + $browser.defer(callback); + } else { + $rootScope.$evalAsync(callback); + } deferred.notify(iteration++); if (count > 0 && iteration >= count) { @@ -8450,24 +13378,33 @@ intervals[promise.$$intervalId] = deferred; return promise; + + function callback() { + if (!hasParams) { + fn(iteration); + } else { + fn.apply(null, args); + } + } } /** - * @ngdoc function - * @name ng.$interval#cancel - * @methodOf ng.$interval + * @ngdoc method + * @name $interval#cancel * * @description * Cancels a task associated with the `promise`. * - * @param {number} promise Promise returned by the `$interval` function. + * @param {Promise=} promise returned by the `$interval` function. * @returns {boolean} Returns `true` if the task was successfully canceled. */ interval.cancel = function(promise) { if (promise && promise.$$intervalId in intervals) { + // Interval cancels should not report as unhandled promise. + markQExceptionHandled(intervals[promise.$$intervalId].promise); intervals[promise.$$intervalId].reject('canceled'); - clearInterval(promise.$$intervalId); + $window.clearInterval(promise.$$intervalId); delete intervals[promise.$$intervalId]; return true; } @@ -8479,8 +13416,89 @@ } /** - * @ngdoc object - * @name ng.$locale + * @ngdoc service + * @name $jsonpCallbacks + * @requires $window + * @description + * This service handles the lifecycle of callbacks to handle JSONP requests. + * Override this service if you wish to customise where the callbacks are stored and + * how they vary compared to the requested url. + */ + var $jsonpCallbacksProvider = /** @this */ function() { + this.$get = function() { + var callbacks = angular.callbacks; + var callbackMap = {}; + + function createCallback(callbackId) { + var callback = function(data) { + callback.data = data; + callback.called = true; + }; + callback.id = callbackId; + return callback; + } + + return { + /** + * @ngdoc method + * @name $jsonpCallbacks#createCallback + * @param {string} url the url of the JSONP request + * @returns {string} the callback path to send to the server as part of the JSONP request + * @description + * {@link $httpBackend} calls this method to create a callback and get hold of the path to the callback + * to pass to the server, which will be used to call the callback with its payload in the JSONP response. + */ + createCallback: function(url) { + var callbackId = '_' + (callbacks.$$counter++).toString(36); + var callbackPath = 'angular.callbacks.' + callbackId; + var callback = createCallback(callbackId); + callbackMap[callbackPath] = callbacks[callbackId] = callback; + return callbackPath; + }, + /** + * @ngdoc method + * @name $jsonpCallbacks#wasCalled + * @param {string} callbackPath the path to the callback that was sent in the JSONP request + * @returns {boolean} whether the callback has been called, as a result of the JSONP response + * @description + * {@link $httpBackend} calls this method to find out whether the JSONP response actually called the + * callback that was passed in the request. + */ + wasCalled: function(callbackPath) { + return callbackMap[callbackPath].called; + }, + /** + * @ngdoc method + * @name $jsonpCallbacks#getResponse + * @param {string} callbackPath the path to the callback that was sent in the JSONP request + * @returns {*} the data received from the response via the registered callback + * @description + * {@link $httpBackend} calls this method to get hold of the data that was provided to the callback + * in the JSONP response. + */ + getResponse: function(callbackPath) { + return callbackMap[callbackPath].data; + }, + /** + * @ngdoc method + * @name $jsonpCallbacks#removeCallback + * @param {string} callbackPath the path to the callback that was sent in the JSONP request + * @description + * {@link $httpBackend} calls this method to remove the callback after the JSONP request has + * completed or timed-out. + */ + removeCallback: function(callbackPath) { + var callback = callbackMap[callbackPath]; + delete callbacks[callback.id]; + delete callbackMap[callbackPath]; + } + }; + }; + }; + + /** + * @ngdoc service + * @name $locale * * @description * $locale service provides localization rules for various Angular components. As of right now the @@ -8488,70 +13506,9 @@ * * * `id` – `{string}` – locale id formatted as `languageId-countryId` (e.g. `en-us`) */ - function $LocaleProvider(){ - this.$get = function() { - return { - id: 'en-us', - NUMBER_FORMATS: { - DECIMAL_SEP: '.', - GROUP_SEP: ',', - PATTERNS: [ - { // Decimal Pattern - minInt: 1, - minFrac: 0, - maxFrac: 3, - posPre: '', - posSuf: '', - negPre: '-', - negSuf: '', - gSize: 3, - lgSize: 3 - },{ //Currency Pattern - minInt: 1, - minFrac: 2, - maxFrac: 2, - posPre: '\u00A4', - posSuf: '', - negPre: '(\u00A4', - negSuf: ')', - gSize: 3, - lgSize: 3 - } - ], - CURRENCY_SYM: '$' - }, - - DATETIME_FORMATS: { - MONTH: - 'January,February,March,April,May,June,July,August,September,October,November,December' - .split(','), - SHORTMONTH: 'Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec'.split(','), - DAY: 'Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday'.split(','), - SHORTDAY: 'Sun,Mon,Tue,Wed,Thu,Fri,Sat'.split(','), - AMPMS: ['AM','PM'], - medium: 'MMM d, y h:mm:ss a', - short: 'M/d/yy h:mm a', - fullDate: 'EEEE, MMMM d, y', - longDate: 'MMMM d, y', - mediumDate: 'MMM d, y', - shortDate: 'M/d/yy', - mediumTime: 'h:mm:ss a', - shortTime: 'h:mm a' - }, - - pluralCat: function(num) { - if (num === 1) { - return 'one'; - } - return 'other'; - } - }; - }; - } - - var PATH_MATCH = /^([^\?#]*)(\?([^#]*))?(#(.*))?$/, - DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp': 21}; + var PATH_MATCH = /^([^?#]*)(\?([^#]*))?(#(.*))?$/, + DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp': 21}; var $locationMinErr = minErr('$location'); @@ -8563,7 +13520,7 @@ */ function encodePath(path) { var segments = path.split('/'), - i = segments.length; + i = segments.length; while (i--) { segments[i] = encodeUriSegment(segments[i]); @@ -8572,50 +13529,62 @@ return segments.join('/'); } - function parseAbsoluteUrl(absoluteUrl, locationObj, appBase) { - var parsedUrl = urlResolve(absoluteUrl, appBase); + function parseAbsoluteUrl(absoluteUrl, locationObj) { + var parsedUrl = urlResolve(absoluteUrl); locationObj.$$protocol = parsedUrl.protocol; locationObj.$$host = parsedUrl.hostname; - locationObj.$$port = int(parsedUrl.port) || DEFAULT_PORTS[parsedUrl.protocol] || null; + locationObj.$$port = toInt(parsedUrl.port) || DEFAULT_PORTS[parsedUrl.protocol] || null; } + var DOUBLE_SLASH_REGEX = /^\s*[\\/]{2,}/; + function parseAppUrl(url, locationObj) { - function parseAppUrl(relativeUrl, locationObj, appBase) { - var prefixed = (relativeUrl.charAt(0) !== '/'); - if (prefixed) { - relativeUrl = '/' + relativeUrl; + if (DOUBLE_SLASH_REGEX.test(url)) { + throw $locationMinErr('badpath', 'Invalid url "{0}".', url); } - var match = urlResolve(relativeUrl, appBase); + + var prefixed = (url.charAt(0) !== '/'); + if (prefixed) { + url = '/' + url; + } + var match = urlResolve(url); locationObj.$$path = decodeURIComponent(prefixed && match.pathname.charAt(0) === '/' ? - match.pathname.substring(1) : match.pathname); + match.pathname.substring(1) : match.pathname); locationObj.$$search = parseKeyValue(match.search); locationObj.$$hash = decodeURIComponent(match.hash); // make sure path starts with '/'; - if (locationObj.$$path && locationObj.$$path.charAt(0) != '/') { + if (locationObj.$$path && locationObj.$$path.charAt(0) !== '/') { locationObj.$$path = '/' + locationObj.$$path; } } + function startsWith(str, search) { + return str.slice(0, search.length) === search; + } /** * - * @param {string} begin - * @param {string} whole - * @returns {string} returns text from whole after begin or undefined if it does not begin with - * expected string. + * @param {string} base + * @param {string} url + * @returns {string} returns text from `url` after `base` or `undefined` if it does not begin with + * the expected string. */ - function beginsWith(begin, whole) { - if (whole.indexOf(begin) === 0) { - return whole.substr(begin.length); + function stripBaseUrl(base, url) { + if (startsWith(url, base)) { + return url.substr(base.length); } } function stripHash(url) { var index = url.indexOf('#'); - return index == -1 ? url : url.substr(0, index); + return index === -1 ? url : url.substr(0, index); + } + + function trimEmptyHash(url) { + return url.replace(/(#.+)|#$/, '$1'); } @@ -8630,33 +13599,33 @@ /** - * LocationHtml5Url represents an url + * LocationHtml5Url represents a URL * This object is exposed as $location service when HTML5 mode is enabled and supported * * @constructor * @param {string} appBase application base URL - * @param {string} basePrefix url path prefix + * @param {string} appBaseNoFile application base URL stripped of any filename + * @param {string} basePrefix URL path prefix */ - function LocationHtml5Url(appBase, basePrefix) { + function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) { this.$$html5 = true; basePrefix = basePrefix || ''; - var appBaseNoFile = stripFile(appBase); - parseAbsoluteUrl(appBase, this, appBase); + parseAbsoluteUrl(appBase, this); /** - * Parse given html5 (regular) url string into properties - * @param {string} newAbsoluteUrl HTML5 url + * Parse given HTML5 (regular) URL string into properties + * @param {string} url HTML5 URL * @private */ this.$$parse = function(url) { - var pathUrl = beginsWith(appBaseNoFile, url); + var pathUrl = stripBaseUrl(appBaseNoFile, url); if (!isString(pathUrl)) { throw $locationMinErr('ipthprfx', 'Invalid url "{0}", missing path prefix "{1}".', url, - appBaseNoFile); + appBaseNoFile); } - parseAppUrl(pathUrl, this, appBase); + parseAppUrl(pathUrl, this); if (!this.$$path) { this.$$path = '/'; @@ -8671,98 +13640,126 @@ */ this.$$compose = function() { var search = toKeyValue(this.$$search), - hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : ''; + hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : ''; this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; this.$$absUrl = appBaseNoFile + this.$$url.substr(1); // first char is always '/' + + this.$$urlUpdatedByLocation = true; }; - this.$$rewrite = function(url) { - var appUrl, prevAppUrl; - - if ( (appUrl = beginsWith(appBase, url)) !== undefined ) { - prevAppUrl = appUrl; - if ( (appUrl = beginsWith(basePrefix, appUrl)) !== undefined ) { - return appBaseNoFile + (beginsWith('/', appUrl) || appUrl); - } else { - return appBase + prevAppUrl; - } - } else if ( (appUrl = beginsWith(appBaseNoFile, url)) !== undefined ) { - return appBaseNoFile + appUrl; - } else if (appBaseNoFile == url + '/') { - return appBaseNoFile; + this.$$parseLinkUrl = function(url, relHref) { + if (relHref && relHref[0] === '#') { + // special case for links to hash fragments: + // keep the old url and only replace the hash fragment + this.hash(relHref.slice(1)); + return true; } + var appUrl, prevAppUrl; + var rewrittenUrl; + + + if (isDefined(appUrl = stripBaseUrl(appBase, url))) { + prevAppUrl = appUrl; + if (basePrefix && isDefined(appUrl = stripBaseUrl(basePrefix, appUrl))) { + rewrittenUrl = appBaseNoFile + (stripBaseUrl('/', appUrl) || appUrl); + } else { + rewrittenUrl = appBase + prevAppUrl; + } + } else if (isDefined(appUrl = stripBaseUrl(appBaseNoFile, url))) { + rewrittenUrl = appBaseNoFile + appUrl; + } else if (appBaseNoFile === url + '/') { + rewrittenUrl = appBaseNoFile; + } + if (rewrittenUrl) { + this.$$parse(rewrittenUrl); + } + return !!rewrittenUrl; }; } /** - * LocationHashbangUrl represents url + * LocationHashbangUrl represents URL * This object is exposed as $location service when developer doesn't opt into html5 mode. * It also serves as the base class for html5 mode fallback on legacy browsers. * * @constructor * @param {string} appBase application base URL + * @param {string} appBaseNoFile application base URL stripped of any filename * @param {string} hashPrefix hashbang prefix */ - function LocationHashbangUrl(appBase, hashPrefix) { - var appBaseNoFile = stripFile(appBase); + function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) { - parseAbsoluteUrl(appBase, this, appBase); + parseAbsoluteUrl(appBase, this); /** - * Parse given hashbang url into properties - * @param {string} url Hashbang url + * Parse given hashbang URL into properties + * @param {string} url Hashbang URL * @private */ this.$$parse = function(url) { - var withoutBaseUrl = beginsWith(appBase, url) || beginsWith(appBaseNoFile, url); - var withoutHashUrl = withoutBaseUrl.charAt(0) == '#' - ? beginsWith(hashPrefix, withoutBaseUrl) - : (this.$$html5) - ? withoutBaseUrl - : ''; + var withoutBaseUrl = stripBaseUrl(appBase, url) || stripBaseUrl(appBaseNoFile, url); + var withoutHashUrl; - if (!isString(withoutHashUrl)) { - throw $locationMinErr('ihshprfx', 'Invalid url "{0}", missing hash prefix "{1}".', url, - hashPrefix); + if (!isUndefined(withoutBaseUrl) && withoutBaseUrl.charAt(0) === '#') { + + // The rest of the URL starts with a hash so we have + // got either a hashbang path or a plain hash fragment + withoutHashUrl = stripBaseUrl(hashPrefix, withoutBaseUrl); + if (isUndefined(withoutHashUrl)) { + // There was no hashbang prefix so we just have a hash fragment + withoutHashUrl = withoutBaseUrl; + } + + } else { + // There was no hashbang path nor hash fragment: + // If we are in HTML5 mode we use what is left as the path; + // Otherwise we ignore what is left + if (this.$$html5) { + withoutHashUrl = withoutBaseUrl; + } else { + withoutHashUrl = ''; + if (isUndefined(withoutBaseUrl)) { + appBase = url; + /** @type {?} */ (this).replace(); + } + } } - parseAppUrl(withoutHashUrl, this, appBase); + + parseAppUrl(withoutHashUrl, this); this.$$path = removeWindowsDriveName(this.$$path, withoutHashUrl, appBase); this.$$compose(); /* - * In Windows, on an anchor node on documents loaded from - * the filesystem, the browser will return a pathname - * prefixed with the drive name ('/C:/path') when a - * pathname without a drive is set: - * * a.setAttribute('href', '/foo') - * * a.pathname === '/C:/foo' //true - * - * Inside of Angular, we're always using pathnames that - * do not include drive names for routing. - */ - function removeWindowsDriveName (path, url, base) { + * In Windows, on an anchor node on documents loaded from + * the filesystem, the browser will return a pathname + * prefixed with the drive name ('/C:/path') when a + * pathname without a drive is set: + * * a.setAttribute('href', '/foo') + * * a.pathname === '/C:/foo' //true + * + * Inside of Angular, we're always using pathnames that + * do not include drive names for routing. + */ + function removeWindowsDriveName(path, url, base) { /* - Matches paths for file protocol on windows, - such as /C:/foo/bar, and captures only /foo/bar. - */ - var windowsFilePathExp = /^\/?.*?:(\/.*)/; + Matches paths for file protocol on windows, + such as /C:/foo/bar, and captures only /foo/bar. + */ + var windowsFilePathExp = /^\/[A-Z]:(\/.*)/; var firstPathSegmentMatch; //Get the relative path from the input URL. - if (url.indexOf(base) === 0) { + if (startsWith(url, base)) { url = url.replace(base, ''); } - /* - * The input URL intentionally contains a - * first path segment that ends with a colon. - */ + // The input URL intentionally contains a first path segment that ends with a colon. if (windowsFilePathExp.exec(url)) { return path; } @@ -8773,268 +13770,424 @@ }; /** - * Compose hashbang url and update `absUrl` property + * Compose hashbang URL and update `absUrl` property * @private */ this.$$compose = function() { var search = toKeyValue(this.$$search), - hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : ''; + hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : ''; this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; this.$$absUrl = appBase + (this.$$url ? hashPrefix + this.$$url : ''); + + this.$$urlUpdatedByLocation = true; }; - this.$$rewrite = function(url) { - if(stripHash(appBase) == stripHash(url)) { - return url; + this.$$parseLinkUrl = function(url, relHref) { + if (stripHash(appBase) === stripHash(url)) { + this.$$parse(url); + return true; } + return false; }; } /** - * LocationHashbangUrl represents url + * LocationHashbangUrl represents URL * This object is exposed as $location service when html5 history api is enabled but the browser * does not support it. * * @constructor * @param {string} appBase application base URL + * @param {string} appBaseNoFile application base URL stripped of any filename * @param {string} hashPrefix hashbang prefix */ - function LocationHashbangInHtml5Url(appBase, hashPrefix) { + function LocationHashbangInHtml5Url(appBase, appBaseNoFile, hashPrefix) { this.$$html5 = true; LocationHashbangUrl.apply(this, arguments); - var appBaseNoFile = stripFile(appBase); + this.$$parseLinkUrl = function(url, relHref) { + if (relHref && relHref[0] === '#') { + // special case for links to hash fragments: + // keep the old url and only replace the hash fragment + this.hash(relHref.slice(1)); + return true; + } - this.$$rewrite = function(url) { + var rewrittenUrl; var appUrl; - if ( appBase == stripHash(url) ) { - return url; - } else if ( (appUrl = beginsWith(appBaseNoFile, url)) ) { - return appBase + hashPrefix + appUrl; - } else if ( appBaseNoFile === url + '/') { - return appBaseNoFile; + if (appBase === stripHash(url)) { + rewrittenUrl = url; + } else if ((appUrl = stripBaseUrl(appBaseNoFile, url))) { + rewrittenUrl = appBase + hashPrefix + appUrl; + } else if (appBaseNoFile === url + '/') { + rewrittenUrl = appBaseNoFile; } + if (rewrittenUrl) { + this.$$parse(rewrittenUrl); + } + return !!rewrittenUrl; }; + + this.$$compose = function() { + var search = toKeyValue(this.$$search), + hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : ''; + + this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; + // include hashPrefix in $$absUrl when $$url is empty so IE9 does not reload page because of removal of '#' + this.$$absUrl = appBase + hashPrefix + this.$$url; + + this.$$urlUpdatedByLocation = true; + }; + } - LocationHashbangInHtml5Url.prototype = - LocationHashbangUrl.prototype = - LocationHtml5Url.prototype = { + var locationPrototype = { - /** - * Are we in html5 mode? - * @private - */ - $$html5: false, + /** + * Ensure absolute URL is initialized. + * @private + */ + $$absUrl:'', - /** - * Has any change been replacing ? - * @private - */ - $$replace: false, + /** + * Are we in html5 mode? + * @private + */ + $$html5: false, - /** - * @ngdoc method - * @name ng.$location#absUrl - * @methodOf ng.$location - * - * @description - * This method is getter only. - * - * Return full url representation with all segments encoded according to rules specified in - * {@link http://www.ietf.org/rfc/rfc3986.txt RFC 3986}. - * - * @return {string} full url - */ - absUrl: locationGetter('$$absUrl'), + /** + * Has any change been replacing? + * @private + */ + $$replace: false, - /** - * @ngdoc method - * @name ng.$location#url - * @methodOf ng.$location - * - * @description - * This method is getter / setter. - * - * Return url (e.g. `/path?a=b#hash`) when called without any parameter. - * - * Change path, search and hash, when called with parameter and return `$location`. - * - * @param {string=} url New url without base prefix (e.g. `/path?a=b#hash`) - * @param {string=} replace The path that will be changed - * @return {string} url - */ - url: function(url, replace) { - if (isUndefined(url)) - return this.$$url; + /** + * @ngdoc method + * @name $location#absUrl + * + * @description + * This method is getter only. + * + * Return full URL representation with all segments encoded according to rules specified in + * [RFC 3986](http://www.ietf.org/rfc/rfc3986.txt). + * + * + * ```js + * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo + * var absUrl = $location.absUrl(); + * // => "http://example.com/#/some/path?foo=bar&baz=xoxo" + * ``` + * + * @return {string} full URL + */ + absUrl: locationGetter('$$absUrl'), - var match = PATH_MATCH.exec(url); - if (match[1]) this.path(decodeURIComponent(match[1])); - if (match[2] || match[1]) this.search(match[3] || ''); - this.hash(match[5] || '', replace); + /** + * @ngdoc method + * @name $location#url + * + * @description + * This method is getter / setter. + * + * Return URL (e.g. `/path?a=b#hash`) when called without any parameter. + * + * Change path, search and hash, when called with parameter and return `$location`. + * + * + * ```js + * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo + * var url = $location.url(); + * // => "/some/path?foo=bar&baz=xoxo" + * ``` + * + * @param {string=} url New URL without base prefix (e.g. `/path?a=b#hash`) + * @return {string} url + */ + url: function(url) { + if (isUndefined(url)) { + return this.$$url; + } - return this; - }, + var match = PATH_MATCH.exec(url); + if (match[1] || url === '') this.path(decodeURIComponent(match[1])); + if (match[2] || match[1] || url === '') this.search(match[3] || ''); + this.hash(match[5] || ''); - /** - * @ngdoc method - * @name ng.$location#protocol - * @methodOf ng.$location - * - * @description - * This method is getter only. - * - * Return protocol of current url. - * - * @return {string} protocol of current url - */ - protocol: locationGetter('$$protocol'), + return this; + }, - /** - * @ngdoc method - * @name ng.$location#host - * @methodOf ng.$location - * - * @description - * This method is getter only. - * - * Return host of current url. - * - * @return {string} host of current url. - */ - host: locationGetter('$$host'), + /** + * @ngdoc method + * @name $location#protocol + * + * @description + * This method is getter only. + * + * Return protocol of current URL. + * + * + * ```js + * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo + * var protocol = $location.protocol(); + * // => "http" + * ``` + * + * @return {string} protocol of current URL + */ + protocol: locationGetter('$$protocol'), - /** - * @ngdoc method - * @name ng.$location#port - * @methodOf ng.$location - * - * @description - * This method is getter only. - * - * Return port of current url. - * - * @return {Number} port - */ - port: locationGetter('$$port'), + /** + * @ngdoc method + * @name $location#host + * + * @description + * This method is getter only. + * + * Return host of current URL. + * + * Note: compared to the non-angular version `location.host` which returns `hostname:port`, this returns the `hostname` portion only. + * + * + * ```js + * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo + * var host = $location.host(); + * // => "example.com" + * + * // given URL http://user:password@example.com:8080/#/some/path?foo=bar&baz=xoxo + * host = $location.host(); + * // => "example.com" + * host = location.host; + * // => "example.com:8080" + * ``` + * + * @return {string} host of current URL. + */ + host: locationGetter('$$host'), - /** - * @ngdoc method - * @name ng.$location#path - * @methodOf ng.$location - * - * @description - * This method is getter / setter. - * - * Return path of current url when called without any parameter. - * - * Change path when called with parameter and return `$location`. - * - * Note: Path should always begin with forward slash (/), this method will add the forward slash - * if it is missing. - * - * @param {string=} path New path - * @return {string} path - */ - path: locationGetterSetter('$$path', function(path) { - return path.charAt(0) == '/' ? path : '/' + path; - }), + /** + * @ngdoc method + * @name $location#port + * + * @description + * This method is getter only. + * + * Return port of current URL. + * + * + * ```js + * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo + * var port = $location.port(); + * // => 80 + * ``` + * + * @return {Number} port + */ + port: locationGetter('$$port'), - /** - * @ngdoc method - * @name ng.$location#search - * @methodOf ng.$location - * - * @description - * This method is getter / setter. - * - * Return search part (as object) of current url when called without any parameter. - * - * Change search part when called with parameter and return `$location`. - * - * @param {string|Object.|Object.>} search New search params - string or - * hash object. Hash object may contain an array of values, which will be decoded as duplicates in - * the url. - * - * @param {(string|Array)=} paramValue If `search` is a string, then `paramValue` will override only a - * single search parameter. If `paramValue` is an array, it will set the parameter as a - * comma-separated value. If `paramValue` is `null`, the parameter will be deleted. - * - * @return {string} search - */ - search: function(search, paramValue) { - switch (arguments.length) { - case 0: - return this.$$search; - case 1: - if (isString(search)) { - this.$$search = parseKeyValue(search); - } else if (isObject(search)) { - this.$$search = search; - } else { - throw $locationMinErr('isrcharg', - 'The first argument of the `$location#search()` call must be a string or an object.'); - } - break; - default: - if (isUndefined(paramValue) || paramValue === null) { - delete this.$$search[search]; - } else { - this.$$search[search] = paramValue; - } - } + /** + * @ngdoc method + * @name $location#path + * + * @description + * This method is getter / setter. + * + * Return path of current URL when called without any parameter. + * + * Change path when called with parameter and return `$location`. + * + * Note: Path should always begin with forward slash (/), this method will add the forward slash + * if it is missing. + * + * + * ```js + * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo + * var path = $location.path(); + * // => "/some/path" + * ``` + * + * @param {(string|number)=} path New path + * @return {(string|object)} path if called with no parameters, or `$location` if called with a parameter + */ + path: locationGetterSetter('$$path', function(path) { + path = path !== null ? path.toString() : ''; + return path.charAt(0) === '/' ? path : '/' + path; + }), - this.$$compose(); - return this; - }, + /** + * @ngdoc method + * @name $location#search + * + * @description + * This method is getter / setter. + * + * Return search part (as object) of current URL when called without any parameter. + * + * Change search part when called with parameter and return `$location`. + * + * + * ```js + * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo + * var searchObject = $location.search(); + * // => {foo: 'bar', baz: 'xoxo'} + * + * // set foo to 'yipee' + * $location.search('foo', 'yipee'); + * // $location.search() => {foo: 'yipee', baz: 'xoxo'} + * ``` + * + * @param {string|Object.|Object.>} search New search params - string or + * hash object. + * + * When called with a single argument the method acts as a setter, setting the `search` component + * of `$location` to the specified value. + * + * If the argument is a hash object containing an array of values, these values will be encoded + * as duplicate search parameters in the URL. + * + * @param {(string|Number|Array|boolean)=} paramValue If `search` is a string or number, then `paramValue` + * will override only a single search property. + * + * If `paramValue` is an array, it will override the property of the `search` component of + * `$location` specified via the first argument. + * + * If `paramValue` is `null`, the property specified via the first argument will be deleted. + * + * If `paramValue` is `true`, the property specified via the first argument will be added with no + * value nor trailing equal sign. + * + * @return {Object} If called with no arguments returns the parsed `search` object. If called with + * one or more arguments returns `$location` object itself. + */ + search: function(search, paramValue) { + switch (arguments.length) { + case 0: + return this.$$search; + case 1: + if (isString(search) || isNumber(search)) { + search = search.toString(); + this.$$search = parseKeyValue(search); + } else if (isObject(search)) { + search = copy(search, {}); + // remove object undefined or null properties + forEach(search, function(value, key) { + if (value == null) delete search[key]; + }); - /** - * @ngdoc method - * @name ng.$location#hash - * @methodOf ng.$location - * - * @description - * This method is getter / setter. - * - * Return hash fragment when called without any parameter. - * - * Change hash fragment when called with parameter and return `$location`. - * - * @param {string=} hash New hash fragment - * @return {string} hash - */ - hash: locationGetterSetter('$$hash', identity), + this.$$search = search; + } else { + throw $locationMinErr('isrcharg', + 'The first argument of the `$location#search()` call must be a string or an object.'); + } + break; + default: + if (isUndefined(paramValue) || paramValue === null) { + delete this.$$search[search]; + } else { + this.$$search[search] = paramValue; + } + } + + this.$$compose(); + return this; + }, + + /** + * @ngdoc method + * @name $location#hash + * + * @description + * This method is getter / setter. + * + * Returns the hash fragment when called without any parameters. + * + * Changes the hash fragment when called with a parameter and returns `$location`. + * + * + * ```js + * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo#hashValue + * var hash = $location.hash(); + * // => "hashValue" + * ``` + * + * @param {(string|number)=} hash New hash fragment + * @return {string} hash + */ + hash: locationGetterSetter('$$hash', function(hash) { + return hash !== null ? hash.toString() : ''; + }), + + /** + * @ngdoc method + * @name $location#replace + * + * @description + * If called, all changes to $location during the current `$digest` will replace the current history + * record, instead of adding a new one. + */ + replace: function() { + this.$$replace = true; + return this; + } + }; + + forEach([LocationHashbangInHtml5Url, LocationHashbangUrl, LocationHtml5Url], function(Location) { + Location.prototype = Object.create(locationPrototype); + + /** + * @ngdoc method + * @name $location#state + * + * @description + * This method is getter / setter. + * + * Return the history state object when called without any parameter. + * + * Change the history state object when called with one parameter and return `$location`. + * The state object is later passed to `pushState` or `replaceState`. + * + * NOTE: This method is supported only in HTML5 mode and only in browsers supporting + * the HTML5 History API (i.e. methods `pushState` and `replaceState`). If you need to support + * older browsers (like IE9 or Android < 4.0), don't use this method. + * + * @param {object=} state State object for pushState or replaceState + * @return {object} state + */ + Location.prototype.state = function(state) { + if (!arguments.length) { + return this.$$state; + } + + if (Location !== LocationHtml5Url || !this.$$html5) { + throw $locationMinErr('nostate', 'History API state support is available only ' + + 'in HTML5 mode and only in browsers supporting HTML5 History API'); + } + // The user might modify `stateObject` after invoking `$location.state(stateObject)` + // but we're changing the $$state reference to $browser.state() during the $digest + // so the modification window is narrow. + this.$$state = isUndefined(state) ? null : state; + this.$$urlUpdatedByLocation = true; + + return this; + }; + }); - /** - * @ngdoc method - * @name ng.$location#replace - * @methodOf ng.$location - * - * @description - * If called, all changes to $location during current `$digest` will be replacing current history - * record, instead of adding new one. - */ - replace: function() { - this.$$replace = true; - return this; - } - }; function locationGetter(property) { - return function() { + return /** @this */ function() { return this[property]; }; } function locationGetterSetter(property, preprocess) { - return function(value) { - if (isUndefined(value)) + return /** @this */ function(value) { + if (isUndefined(value)) { return this[property]; + } this[property] = preprocess(value); this.$$compose(); @@ -9045,16 +14198,14 @@ /** - * @ngdoc object - * @name ng.$location + * @ngdoc service + * @name $location * - * @requires $browser - * @requires $sniffer * @requires $rootElement * * @description * The $location service parses the URL in the browser address bar (based on the - * {@link https://developer.mozilla.org/en/window.location window.location}) and makes the URL + * [window.location](https://developer.mozilla.org/en/window.location)) and makes the URL * available to your application. Changes to the URL in the address bar are reflected into * $location service and changes to $location are reflected into the browser address bar. * @@ -9069,25 +14220,30 @@ * - Clicks on a link. * - Represents the URL object as a set of methods (protocol, host, port, path, search, hash). * - * For more information see {@link guide/dev_guide.services.$location Developer Guide: Angular - * Services: Using $location} + * For more information see {@link guide/$location Developer Guide: Using $location} */ /** - * @ngdoc object - * @name ng.$locationProvider + * @ngdoc provider + * @name $locationProvider + * @this + * * @description * Use the `$locationProvider` to configure how the application deep linking paths are stored. */ - function $LocationProvider(){ - var hashPrefix = '', - html5Mode = false; + function $LocationProvider() { + var hashPrefix = '!', + html5Mode = { + enabled: false, + requireBase: true, + rewriteLinks: true + }; /** - * @ngdoc property - * @name ng.$locationProvider#hashPrefix - * @methodOf ng.$locationProvider + * @ngdoc method + * @name $locationProvider#hashPrefix * @description + * The default value for the prefix is `'!'`. * @param {string=} prefix Prefix for hash part (containing path and search) * @returns {*} current value if used as getter or itself (chaining) if used as setter */ @@ -9101,16 +14257,46 @@ }; /** - * @ngdoc property - * @name ng.$locationProvider#html5Mode - * @methodOf ng.$locationProvider + * @ngdoc method + * @name $locationProvider#html5Mode * @description - * @param {boolean=} mode Use HTML5 strategy if available. - * @returns {*} current value if used as getter or itself (chaining) if used as setter + * @param {(boolean|Object)=} mode If boolean, sets `html5Mode.enabled` to value. + * If object, sets `enabled`, `requireBase` and `rewriteLinks` to respective values. Supported + * properties: + * - **enabled** – `{boolean}` – (default: false) If true, will rely on `history.pushState` to + * change urls where supported. Will fall back to hash-prefixed paths in browsers that do not + * support `pushState`. + * - **requireBase** - `{boolean}` - (default: `true`) When html5Mode is enabled, specifies + * whether or not a tag is required to be present. If `enabled` and `requireBase` are + * true, and a base tag is not present, an error will be thrown when `$location` is injected. + * See the {@link guide/$location $location guide for more information} + * - **rewriteLinks** - `{boolean|string}` - (default: `true`) When html5Mode is enabled, + * enables/disables URL rewriting for relative links. If set to a string, URL rewriting will + * only happen on links with an attribute that matches the given string. For example, if set + * to `'internal-link'`, then the URL will only be rewritten for `` links. + * Note that [attribute name normalization](guide/directive#normalization) does not apply + * here, so `'internalLink'` will **not** match `'internal-link'`. + * + * @returns {Object} html5Mode object if used as getter or itself (chaining) if used as setter */ this.html5Mode = function(mode) { - if (isDefined(mode)) { - html5Mode = mode; + if (isBoolean(mode)) { + html5Mode.enabled = mode; + return this; + } else if (isObject(mode)) { + + if (isBoolean(mode.enabled)) { + html5Mode.enabled = mode.enabled; + } + + if (isBoolean(mode.requireBase)) { + html5Mode.requireBase = mode.requireBase; + } + + if (isBoolean(mode.rewriteLinks) || isString(mode.rewriteLinks)) { + html5Mode.rewriteLinks = mode.rewriteLinks; + } + return this; } else { return html5Mode; @@ -9119,66 +14305,111 @@ /** * @ngdoc event - * @name ng.$location#$locationChangeStart - * @eventOf ng.$location + * @name $location#$locationChangeStart * @eventType broadcast on root scope * @description - * Broadcasted before a URL will change. This change can be prevented by calling + * Broadcasted before a URL will change. + * + * This change can be prevented by calling * `preventDefault` method of the event. See {@link ng.$rootScope.Scope#$on} for more * details about event object. Upon successful change - * {@link ng.$location#events_$locationChangeSuccess $locationChangeSuccess} is fired. + * {@link ng.$location#$locationChangeSuccess $locationChangeSuccess} is fired. + * + * The `newState` and `oldState` parameters may be defined only in HTML5 mode and when + * the browser supports the HTML5 History API. * * @param {Object} angularEvent Synthetic event object. * @param {string} newUrl New URL * @param {string=} oldUrl URL that was before it was changed. + * @param {string=} newState New history state object + * @param {string=} oldState History state object that was before it was changed. */ /** * @ngdoc event - * @name ng.$location#$locationChangeSuccess - * @eventOf ng.$location + * @name $location#$locationChangeSuccess * @eventType broadcast on root scope * @description * Broadcasted after a URL was changed. * + * The `newState` and `oldState` parameters may be defined only in HTML5 mode and when + * the browser supports the HTML5 History API. + * * @param {Object} angularEvent Synthetic event object. * @param {string} newUrl New URL * @param {string=} oldUrl URL that was before it was changed. + * @param {string=} newState New history state object + * @param {string=} oldState History state object that was before it was changed. */ - this.$get = ['$rootScope', '$browser', '$sniffer', '$rootElement', - function( $rootScope, $browser, $sniffer, $rootElement) { + this.$get = ['$rootScope', '$browser', '$sniffer', '$rootElement', '$window', + function($rootScope, $browser, $sniffer, $rootElement, $window) { var $location, - LocationMode, - baseHref = $browser.baseHref(), // if base[href] is undefined, it defaults to '' - initialUrl = $browser.url(), - appBase; + LocationMode, + baseHref = $browser.baseHref(), // if base[href] is undefined, it defaults to '' + initialUrl = $browser.url(), + appBase; - if (html5Mode) { + if (html5Mode.enabled) { + if (!baseHref && html5Mode.requireBase) { + throw $locationMinErr('nobase', + '$location in HTML5 mode requires a tag to be present!'); + } appBase = serverBase(initialUrl) + (baseHref || '/'); LocationMode = $sniffer.history ? LocationHtml5Url : LocationHashbangInHtml5Url; } else { appBase = stripHash(initialUrl); LocationMode = LocationHashbangUrl; } - $location = new LocationMode(appBase, '#' + hashPrefix); - $location.$$parse($location.$$rewrite(initialUrl)); + var appBaseNoFile = stripFile(appBase); + + $location = new LocationMode(appBase, appBaseNoFile, '#' + hashPrefix); + $location.$$parseLinkUrl(initialUrl, initialUrl); + + $location.$$state = $browser.state(); + + var IGNORE_URI_REGEXP = /^\s*(javascript|mailto):/i; + + function setBrowserUrlWithFallback(url, replace, state) { + var oldUrl = $location.url(); + var oldState = $location.$$state; + try { + $browser.url(url, replace, state); + + // Make sure $location.state() returns referentially identical (not just deeply equal) + // state object; this makes possible quick checking if the state changed in the digest + // loop. Checking deep equality would be too expensive. + $location.$$state = $browser.state(); + } catch (e) { + // Restore old values if pushState fails + $location.url(oldUrl); + $location.$$state = oldState; + + throw e; + } + } $rootElement.on('click', function(event) { + var rewriteLinks = html5Mode.rewriteLinks; // TODO(vojta): rewrite link when opening in new tab/window (in legacy browser) // currently we open nice url link and redirect then - if (event.ctrlKey || event.metaKey || event.which == 2) return; + if (!rewriteLinks || event.ctrlKey || event.metaKey || event.shiftKey || event.which === 2 || event.button === 2) return; var elm = jqLite(event.target); // traverse the DOM up to find first A tag - while (lowercase(elm[0].nodeName) !== 'a') { + while (nodeName_(elm[0]) !== 'a') { // ignore rewriting if no A tag (reached root element, or no parent - removed from document) if (elm[0] === $rootElement[0] || !(elm = elm.parent())[0]) return; } + if (isString(rewriteLinks) && isUndefined(elm.attr(rewriteLinks))) return; + var absHref = elm.prop('href'); + // get the actual href attribute - see + // http://msdn.microsoft.com/en-us/library/ie/dd347148(v=vs.85).aspx + var relHref = elm.attr('href') || elm.attr('xlink:href'); if (isObject(absHref) && absHref.toString() === '[object SVGAnimatedString]') { // SVGAnimatedString.animVal should be identical to SVGAnimatedString.baseVal, unless during @@ -9186,79 +14417,125 @@ absHref = urlResolve(absHref.animVal).href; } - var rewrittenUrl = $location.$$rewrite(absHref); + // Ignore when url is started with javascript: or mailto: + if (IGNORE_URI_REGEXP.test(absHref)) return; - if (absHref && !elm.attr('target') && rewrittenUrl && !event.isDefaultPrevented()) { - event.preventDefault(); - if (rewrittenUrl != $browser.url()) { + if (absHref && !elm.attr('target') && !event.isDefaultPrevented()) { + if ($location.$$parseLinkUrl(absHref, relHref)) { + // We do a preventDefault for all urls that are part of the angular application, + // in html5mode and also without, so that we are able to abort navigation without + // getting double entries in the location history. + event.preventDefault(); // update location manually - $location.$$parse(rewrittenUrl); - $rootScope.$apply(); - // hack to work around FF6 bug 684208 when scenario runner clicks on links - window.angular['ff-684208-preventDefault'] = true; + if ($location.absUrl() !== $browser.url()) { + $rootScope.$apply(); + // hack to work around FF6 bug 684208 when scenario runner clicks on links + $window.angular['ff-684208-preventDefault'] = true; + } } } }); // rewrite hashbang url <> html5 url - if ($location.absUrl() != initialUrl) { + if (trimEmptyHash($location.absUrl()) !== trimEmptyHash(initialUrl)) { $browser.url($location.absUrl(), true); } - // update $location when $browser url changes - $browser.onUrlChange(function(newUrl) { - if ($location.absUrl() != newUrl) { - $rootScope.$evalAsync(function() { - var oldUrl = $location.absUrl(); + var initializing = true; - $location.$$parse(newUrl); - if ($rootScope.$broadcast('$locationChangeStart', newUrl, - oldUrl).defaultPrevented) { - $location.$$parse(oldUrl); - $browser.url(oldUrl); - } else { - afterLocationChange(oldUrl); - } - }); - if (!$rootScope.$$phase) $rootScope.$digest(); + // update $location when $browser url changes + $browser.onUrlChange(function(newUrl, newState) { + + if (!startsWith(newUrl, appBaseNoFile)) { + // If we are navigating outside of the app then force a reload + $window.location.href = newUrl; + return; } + + $rootScope.$evalAsync(function() { + var oldUrl = $location.absUrl(); + var oldState = $location.$$state; + var defaultPrevented; + newUrl = trimEmptyHash(newUrl); + $location.$$parse(newUrl); + $location.$$state = newState; + + defaultPrevented = $rootScope.$broadcast('$locationChangeStart', newUrl, oldUrl, + newState, oldState).defaultPrevented; + + // if the location was changed by a `$locationChangeStart` handler then stop + // processing this location change + if ($location.absUrl() !== newUrl) return; + + if (defaultPrevented) { + $location.$$parse(oldUrl); + $location.$$state = oldState; + setBrowserUrlWithFallback(oldUrl, false, oldState); + } else { + initializing = false; + afterLocationChange(oldUrl, oldState); + } + }); + if (!$rootScope.$$phase) $rootScope.$digest(); }); // update browser - var changeCounter = 0; $rootScope.$watch(function $locationWatch() { - var oldUrl = $browser.url(); - var currentReplace = $location.$$replace; + if (initializing || $location.$$urlUpdatedByLocation) { + $location.$$urlUpdatedByLocation = false; - if (!changeCounter || oldUrl != $location.absUrl()) { - changeCounter++; - $rootScope.$evalAsync(function() { - if ($rootScope.$broadcast('$locationChangeStart', $location.absUrl(), oldUrl). - defaultPrevented) { - $location.$$parse(oldUrl); - } else { - $browser.url($location.absUrl(), currentReplace); - afterLocationChange(oldUrl); - } - }); + var oldUrl = trimEmptyHash($browser.url()); + var newUrl = trimEmptyHash($location.absUrl()); + var oldState = $browser.state(); + var currentReplace = $location.$$replace; + var urlOrStateChanged = oldUrl !== newUrl || + ($location.$$html5 && $sniffer.history && oldState !== $location.$$state); + + if (initializing || urlOrStateChanged) { + initializing = false; + + $rootScope.$evalAsync(function() { + var newUrl = $location.absUrl(); + var defaultPrevented = $rootScope.$broadcast('$locationChangeStart', newUrl, oldUrl, + $location.$$state, oldState).defaultPrevented; + + // if the location was changed by a `$locationChangeStart` handler then stop + // processing this location change + if ($location.absUrl() !== newUrl) return; + + if (defaultPrevented) { + $location.$$parse(oldUrl); + $location.$$state = oldState; + } else { + if (urlOrStateChanged) { + setBrowserUrlWithFallback(newUrl, currentReplace, + oldState === $location.$$state ? null : $location.$$state); + } + afterLocationChange(oldUrl, oldState); + } + }); + } } + $location.$$replace = false; - return changeCounter; + // we don't need to return anything because $evalAsync will make the digest loop dirty when + // there is a change }); return $location; - function afterLocationChange(oldUrl) { - $rootScope.$broadcast('$locationChangeSuccess', $location.absUrl(), oldUrl); + function afterLocationChange(oldUrl, oldState) { + $rootScope.$broadcast('$locationChangeSuccess', $location.absUrl(), oldUrl, + $location.$$state, oldState); } }]; } /** - * @ngdoc object - * @name ng.$log + * @ngdoc service + * @name $log * @requires $window * * @description @@ -9267,47 +14544,58 @@ * * The main purpose of this service is to simplify debugging and troubleshooting. * + * To reveal the location of the calls to `$log` in the JavaScript console, + * you can "blackbox" the AngularJS source in your browser: + * + * [Mozilla description of blackboxing](https://developer.mozilla.org/en-US/docs/Tools/Debugger/How_to/Black_box_a_source). + * [Chrome description of blackboxing](https://developer.chrome.com/devtools/docs/blackboxing). + * + * Note: Not all browsers support blackboxing. + * * The default is to log `debug` messages. You can use * {@link ng.$logProvider ng.$logProvider#debugEnabled} to change this. * * @example - + - function LogCtrl($scope, $log) { - $scope.$log = $log; - $scope.message = 'Hello World!'; - } + angular.module('logExample', []) + .controller('LogController', ['$scope', '$log', function($scope, $log) { + $scope.$log = $log; + $scope.message = 'Hello World!'; + }]); -
    +

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

    - Message: - + +
    */ /** - * @ngdoc object - * @name ng.$logProvider + * @ngdoc provider + * @name $logProvider + * @this + * * @description * Use the `$logProvider` to configure how the application logs messages */ - function $LogProvider(){ + function $LogProvider() { var debug = true, - self = this; + self = this; /** - * @ngdoc property - * @name ng.$logProvider#debugEnabled - * @methodOf ng.$logProvider + * @ngdoc method + * @name $logProvider#debugEnabled * @description - * @param {string=} flag enable or disable debug level messages + * @param {boolean=} flag enable or disable debug level messages * @returns {*} current value if used as getter or itself (chaining) if used as setter */ this.debugEnabled = function(flag) { @@ -9319,12 +14607,20 @@ } }; - this.$get = ['$window', function($window){ + this.$get = ['$window', function($window) { + // Support: IE 9-11, Edge 12-14+ + // IE/Edge display errors in such a way that it requires the user to click in 4 places + // to see the stack trace. There is no way to feature-detect it so there's a chance + // of the user agent sniffing to go wrong but since it's only about logging, this shouldn't + // break apps. Other browsers display errors in a sensible way and some of them map stack + // traces along source maps if available so it makes sense to let browsers display it + // as they want. + var formatStackTrace = msie || /\bEdge\//.test($window.navigator && $window.navigator.userAgent); + return { /** * @ngdoc method - * @name ng.$log#log - * @methodOf ng.$log + * @name $log#log * * @description * Write a log message @@ -9333,8 +14629,7 @@ /** * @ngdoc method - * @name ng.$log#info - * @methodOf ng.$log + * @name $log#info * * @description * Write an information message @@ -9343,8 +14638,7 @@ /** * @ngdoc method - * @name ng.$log#warn - * @methodOf ng.$log + * @name $log#warn * * @description * Write a warning message @@ -9353,8 +14647,7 @@ /** * @ngdoc method - * @name ng.$log#error - * @methodOf ng.$log + * @name $log#error * * @description * Write an error message @@ -9363,13 +14656,12 @@ /** * @ngdoc method - * @name ng.$log#debug - * @methodOf ng.$log + * @name $log#debug * * @description * Write a debug message */ - debug: (function () { + debug: (function() { var fn = consoleLog('debug'); return function() { @@ -9377,15 +14669,15 @@ fn.apply(self, arguments); } }; - }()) + })() }; function formatError(arg) { - if (arg instanceof Error) { - if (arg.stack) { + if (isError(arg)) { + if (arg.stack && formatStackTrace) { arg = (arg.message && arg.stack.indexOf(arg.message) === -1) - ? 'Error: ' + arg.message + '\n' + arg.stack - : arg.stack; + ? 'Error: ' + arg.message + '\n' + arg.stack + : arg.stack; } else if (arg.sourceURL) { arg = arg.message + '\n' + arg.sourceURL + ':' + arg.line; } @@ -9395,139 +14687,74 @@ function consoleLog(type) { var console = $window.console || {}, - logFn = console[type] || console.log || noop, - hasApply = false; + logFn = console[type] || console.log || noop; - // Note: reading logFn.apply throws an error in IE11 in IE8 document mode. - // The reason behind this is that console.log has type "object" in IE8... - try { - hasApply = !! logFn.apply; - } catch (e) {} - - if (hasApply) { - return function() { - var args = []; - forEach(arguments, function(arg) { - args.push(formatError(arg)); - }); - return logFn.apply(console, args); - }; - } - - // we are IE which either doesn't have window.console => this is noop and we do nothing, - // or we are IE where console.log doesn't have apply so we log at least first 2 args - return function(arg1, arg2) { - logFn(arg1, arg2 == null ? '' : arg2); + return function() { + var args = []; + forEach(arguments, function(arg) { + args.push(formatError(arg)); + }); + // Support: IE 9 only + // console methods don't inherit from Function.prototype in IE 9 so we can't + // call `logFn.apply(console, args)` directly. + return Function.prototype.apply.call(logFn, console, args); }; } }]; } + /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * Any commits to this file should be reviewed with security in mind. * + * Changes to this file can potentially create security vulnerabilities. * + * An approval from 2 Core members with history of modifying * + * this file is required. * + * * + * Does the change somehow allow for arbitrary javascript to be executed? * + * Or allows for someone to change the prototype of built-in objects? * + * Or gives undesired access to variables likes document or window? * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + var $parseMinErr = minErr('$parse'); - var promiseWarningCache = {}; - var promiseWarning; + + var objectValueOf = {}.constructor.prototype.valueOf; // Sandboxing Angular Expressions // ------------------------------ -// Angular expressions are generally considered safe because these expressions only have direct -// access to $scope and locals. However, one can obtain the ability to execute arbitrary JS code by -// obtaining a reference to native JS functions such as the Function constructor. +// Angular expressions are no longer sandboxed. So it is now even easier to access arbitrary JS code by +// various means such as obtaining a reference to native JS functions like the Function constructor. // // As an example, consider the following Angular expression: // -// {}.toString.constructor(alert("evil JS code")) +// {}.toString.constructor('alert("evil JS code")') // -// We want to prevent this type of access. For the sake of performance, during the lexing phase we -// disallow any "dotted" access to any member named "constructor". +// It is important to realize that if you create an expression from a string that contains user provided +// content then it is possible that your application contains a security vulnerability to an XSS style attack. // -// For reflective calls (a[b]) we check that the value of the lookup is not the Function constructor -// while evaluating the expression, which is a stronger but more expensive test. Since reflective -// calls are expensive anyway, this is not such a big deal compared to static dereferencing. -// -// This sandboxing technique is not perfect and doesn't aim to be. The goal is to prevent exploits -// against the expression language, but not to prevent exploits that were enabled by exposing -// sensitive JavaScript or browser apis on Scope. Exposing such objects on a Scope is never a good -// practice and therefore we are not even trying to protect against interaction with an object -// explicitly exposed in this way. -// -// A developer could foil the name check by aliasing the Function constructor under a different -// name on the scope. -// -// In general, it is not possible to access a Window object from an angular expression unless a -// window or some DOM object that has a reference to window is published onto a Scope. +// See https://docs.angularjs.org/guide/security - function ensureSafeMemberName(name, fullExpression) { - if (name === "constructor") { - throw $parseMinErr('isecfld', - 'Referencing "constructor" field in Angular expressions is disallowed! Expression: {0}', - fullExpression); - } - return name; + + function getStringValue(name) { + // Property names must be strings. This means that non-string objects cannot be used + // as keys in an object. Any non-string object, including a number, is typecasted + // into a string via the toString method. + // -- MDN, https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Property_accessors#Property_names + // + // So, to ensure that we are checking the same `name` that JavaScript would use, we cast it + // to a string. It's not always possible. If `name` is an object and its `toString` method is + // 'broken' (doesn't return a string, isn't a function, etc.), an error will be thrown: + // + // TypeError: Cannot convert object to primitive value + // + // For performance reasons, we don't catch this error here and allow it to propagate up the call + // stack. Note that you'll get the same error in JavaScript if you try to access a property using + // such a 'broken' object as a key. + return name + ''; } - function ensureSafeObject(obj, fullExpression) { - // nifty check if obj is Function that is fast and works across iframes and other contexts - if (obj) { - if (obj.constructor === obj) { - throw $parseMinErr('isecfn', - 'Referencing Function in Angular expressions is disallowed! Expression: {0}', - fullExpression); - } else if (// isWindow(obj) - obj.document && obj.location && obj.alert && obj.setInterval) { - throw $parseMinErr('isecwindow', - 'Referencing the Window in Angular expressions is disallowed! Expression: {0}', - fullExpression); - } else if (// isElement(obj) - obj.children && (obj.nodeName || (obj.on && obj.find))) { - throw $parseMinErr('isecdom', - 'Referencing DOM nodes in Angular expressions is disallowed! Expression: {0}', - fullExpression); - } - } - return obj; - } - var OPERATORS = { - /* jshint bitwise : false */ - 'null':function(){return null;}, - 'true':function(){return true;}, - 'false':function(){return false;}, - undefined:noop, - '+':function(self, locals, a,b){ - a=a(self, locals); b=b(self, locals); - if (isDefined(a)) { - if (isDefined(b)) { - return a + b; - } - return a; - } - return isDefined(b)?b:undefined;}, - '-':function(self, locals, a,b){ - a=a(self, locals); b=b(self, locals); - return (isDefined(a)?a:0)-(isDefined(b)?b:0); - }, - '*':function(self, locals, a,b){return a(self, locals)*b(self, locals);}, - '/':function(self, locals, a,b){return a(self, locals)/b(self, locals);}, - '%':function(self, locals, a,b){return a(self, locals)%b(self, locals);}, - '^':function(self, locals, a,b){return a(self, locals)^b(self, locals);}, - '=':noop, - '===':function(self, locals, a, b){return a(self, locals)===b(self, locals);}, - '!==':function(self, locals, a, b){return a(self, locals)!==b(self, locals);}, - '==':function(self, locals, a,b){return a(self, locals)==b(self, locals);}, - '!=':function(self, locals, a,b){return a(self, locals)!=b(self, locals);}, - '<':function(self, locals, a,b){return a(self, locals)':function(self, locals, a,b){return a(self, locals)>b(self, locals);}, - '<=':function(self, locals, a,b){return a(self, locals)<=b(self, locals);}, - '>=':function(self, locals, a,b){return a(self, locals)>=b(self, locals);}, - '&&':function(self, locals, a,b){return a(self, locals)&&b(self, locals);}, - '||':function(self, locals, a,b){return a(self, locals)||b(self, locals);}, - '&':function(self, locals, a,b){return a(self, locals)&b(self, locals);}, -// '|':function(self, locals, a,b){return a|b;}, - '|':function(self, locals, a,b){return b(self, locals)(self, locals, a(self, locals));}, - '!':function(self, locals, a){return !a(self, locals);} - }; - /* jshint bitwise: true */ - var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'}; + var OPERATORS = createMap(); + forEach('+ - * / % === !== == != < > <= >= && || ! = |'.split(' '), function(operator) { OPERATORS[operator] = true; }); + var ESCAPE = {'n':'\n', 'f':'\f', 'r':'\r', 't':'\t', 'v':'\v', '\'':'\'', '"':'"'}; ///////////////////////////////////////// @@ -9536,85 +14763,51 @@ /** * @constructor */ - var Lexer = function (options) { + var Lexer = function Lexer(options) { this.options = options; }; Lexer.prototype = { constructor: Lexer, - lex: function (text) { + lex: function(text) { this.text = text; - this.index = 0; - this.ch = undefined; - this.lastCh = ':'; // can start regexp - this.tokens = []; - var token; - var json = []; - while (this.index < this.text.length) { - this.ch = this.text.charAt(this.index); - if (this.is('"\'')) { - this.readString(this.ch); - } else if (this.isNumber(this.ch) || this.is('.') && this.isNumber(this.peek())) { + var ch = this.text.charAt(this.index); + if (ch === '"' || ch === '\'') { + this.readString(ch); + } else if (this.isNumber(ch) || ch === '.' && this.isNumber(this.peek())) { this.readNumber(); - } else if (this.isIdent(this.ch)) { + } else if (this.isIdentifierStart(this.peekMultichar())) { this.readIdent(); - // identifiers can only be if the preceding char was a { or , - if (this.was('{,') && json[0] === '{' && - (token = this.tokens[this.tokens.length - 1])) { - token.json = token.text.indexOf('.') === -1; - } - } else if (this.is('(){}[].,;:?')) { - this.tokens.push({ - index: this.index, - text: this.ch, - json: (this.was(':[,') && this.is('{[')) || this.is('}]:,') - }); - if (this.is('{[')) json.unshift(this.ch); - if (this.is('}]')) json.shift(); + } else if (this.is(ch, '(){}[].,;:?')) { + this.tokens.push({index: this.index, text: ch}); this.index++; - } else if (this.isWhitespace(this.ch)) { + } else if (this.isWhitespace(ch)) { this.index++; - continue; } else { - var ch2 = this.ch + this.peek(); + var ch2 = ch + this.peek(); var ch3 = ch2 + this.peek(2); - var fn = OPERATORS[this.ch]; - var fn2 = OPERATORS[ch2]; - var fn3 = OPERATORS[ch3]; - if (fn3) { - this.tokens.push({index: this.index, text: ch3, fn: fn3}); - this.index += 3; - } else if (fn2) { - this.tokens.push({index: this.index, text: ch2, fn: fn2}); - this.index += 2; - } else if (fn) { - this.tokens.push({ - index: this.index, - text: this.ch, - fn: fn, - json: (this.was('[,:') && this.is('+-')) - }); - this.index += 1; + var op1 = OPERATORS[ch]; + var op2 = OPERATORS[ch2]; + var op3 = OPERATORS[ch3]; + if (op1 || op2 || op3) { + var token = op3 ? ch3 : (op2 ? ch2 : ch); + this.tokens.push({index: this.index, text: token, operator: true}); + this.index += token.length; } else { this.throwError('Unexpected next character ', this.index, this.index + 1); } } - this.lastCh = this.ch; } return this.tokens; }, - is: function(chars) { - return chars.indexOf(this.ch) !== -1; - }, - - was: function(chars) { - return chars.indexOf(this.lastCh) !== -1; + is: function(ch, chars) { + return chars.indexOf(ch) !== -1; }, peek: function(i) { @@ -9623,19 +14816,55 @@ }, isNumber: function(ch) { - return ('0' <= ch && ch <= '9'); + return ('0' <= ch && ch <= '9') && typeof ch === 'string'; }, isWhitespace: function(ch) { // IE treats non-breaking space as \u00A0 return (ch === ' ' || ch === '\r' || ch === '\t' || - ch === '\n' || ch === '\v' || ch === '\u00A0'); + ch === '\n' || ch === '\v' || ch === '\u00A0'); }, - isIdent: function(ch) { + isIdentifierStart: function(ch) { + return this.options.isIdentifierStart ? + this.options.isIdentifierStart(ch, this.codePointAt(ch)) : + this.isValidIdentifierStart(ch); + }, + + isValidIdentifierStart: function(ch) { return ('a' <= ch && ch <= 'z' || - 'A' <= ch && ch <= 'Z' || - '_' === ch || ch === '$'); + 'A' <= ch && ch <= 'Z' || + '_' === ch || ch === '$'); + }, + + isIdentifierContinue: function(ch) { + return this.options.isIdentifierContinue ? + this.options.isIdentifierContinue(ch, this.codePointAt(ch)) : + this.isValidIdentifierContinue(ch); + }, + + isValidIdentifierContinue: function(ch, cp) { + return this.isValidIdentifierStart(ch, cp) || this.isNumber(ch); + }, + + codePointAt: function(ch) { + if (ch.length === 1) return ch.charCodeAt(0); + // eslint-disable-next-line no-bitwise + return (ch.charCodeAt(0) << 10) + ch.charCodeAt(1) - 0x35FDC00; + }, + + peekMultichar: function() { + var ch = this.text.charAt(this.index); + var peek = this.peek(); + if (!peek) { + return ch; + } + var cp1 = ch.charCodeAt(0); + var cp2 = peek.charCodeAt(0); + if (cp1 >= 0xD800 && cp1 <= 0xDBFF && cp2 >= 0xDC00 && cp2 <= 0xDFFF) { + return ch + peek; + } + return ch; }, isExpOperator: function(ch) { @@ -9645,10 +14874,10 @@ throwError: function(error, start, end) { end = end || this.index; var colStr = (isDefined(start) - ? 's ' + start + '-' + this.index + ' [' + this.text.substring(start, end) + ']' - : ' ' + end); + ? 's ' + start + '-' + this.index + ' [' + this.text.substring(start, end) + ']' + : ' ' + end); throw $parseMinErr('lexerr', 'Lexer Error: {0} at column{1} in expression [{2}].', - error, colStr, this.text); + error, colStr, this.text); }, readNumber: function() { @@ -9656,19 +14885,19 @@ var start = this.index; while (this.index < this.text.length) { var ch = lowercase(this.text.charAt(this.index)); - if (ch == '.' || this.isNumber(ch)) { + if (ch === '.' || this.isNumber(ch)) { number += ch; } else { var peekCh = this.peek(); - if (ch == 'e' && this.isExpOperator(peekCh)) { + if (ch === 'e' && this.isExpOperator(peekCh)) { number += ch; } else if (this.isExpOperator(ch) && - peekCh && this.isNumber(peekCh) && - number.charAt(number.length - 1) == 'e') { + peekCh && this.isNumber(peekCh) && + number.charAt(number.length - 1) === 'e') { number += ch; } else if (this.isExpOperator(ch) && - (!peekCh || !this.isNumber(peekCh)) && - number.charAt(number.length - 1) == 'e') { + (!peekCh || !this.isNumber(peekCh)) && + number.charAt(number.length - 1) === 'e') { this.throwError('Invalid exponent'); } else { break; @@ -9676,88 +14905,29 @@ } this.index++; } - number = 1 * number; this.tokens.push({ index: start, text: number, - json: true, - fn: function() { return number; } + constant: true, + value: Number(number) }); }, readIdent: function() { - var parser = this; - - var ident = ''; var start = this.index; - - var lastDot, peekIndex, methodName, ch; - + this.index += this.peekMultichar().length; while (this.index < this.text.length) { - ch = this.text.charAt(this.index); - if (ch === '.' || this.isIdent(ch) || this.isNumber(ch)) { - if (ch === '.') lastDot = this.index; - ident += ch; - } else { + var ch = this.peekMultichar(); + if (!this.isIdentifierContinue(ch)) { break; } - this.index++; + this.index += ch.length; } - - //check if this is not a method invocation and if it is back out to last dot - if (lastDot) { - peekIndex = this.index; - while (peekIndex < this.text.length) { - ch = this.text.charAt(peekIndex); - if (ch === '(') { - methodName = ident.substr(lastDot - start + 1); - ident = ident.substr(0, lastDot - start); - this.index = peekIndex; - break; - } - if (this.isWhitespace(ch)) { - peekIndex++; - } else { - break; - } - } - } - - - var token = { + this.tokens.push({ index: start, - text: ident - }; - - // OPERATORS is our own object so we don't need to use special hasOwnPropertyFn - if (OPERATORS.hasOwnProperty(ident)) { - token.fn = OPERATORS[ident]; - token.json = OPERATORS[ident]; - } else { - var getter = getterFn(ident, this.options, this.text); - token.fn = extend(function(self, locals) { - return (getter(self, locals)); - }, { - assign: function(self, value) { - return setter(self, ident, value, parser.text, parser.options); - } - }); - } - - this.tokens.push(token); - - if (methodName) { - this.tokens.push({ - index:lastDot, - text: '.', - json: false - }); - this.tokens.push({ - index: lastDot + 1, - text: methodName, - json: false - }); - } + text: this.text.slice(start, this.index), + identifier: true + }); }, readString: function(quote) { @@ -9772,17 +14942,14 @@ if (escape) { if (ch === 'u') { var hex = this.text.substring(this.index + 1, this.index + 5); - if (!hex.match(/[\da-f]{4}/i)) + if (!hex.match(/[\da-f]{4}/i)) { this.throwError('Invalid unicode escape [\\u' + hex + ']'); + } this.index += 4; string += String.fromCharCode(parseInt(hex, 16)); } else { var rep = ESCAPE[ch]; - if (rep) { - string += rep; - } else { - string += ch; - } + string = string + (rep || ch); } escape = false; } else if (ch === '\\') { @@ -9792,9 +14959,8 @@ this.tokens.push({ index: start, text: rawString, - string: string, - json: true, - fn: function() { return string; } + constant: true, + value: string }); return; } else { @@ -9806,215 +14972,66 @@ } }; - - /** - * @constructor - */ - var Parser = function (lexer, $filter, options) { + var AST = function AST(lexer, options) { this.lexer = lexer; - this.$filter = $filter; this.options = options; }; - Parser.ZERO = function () { return 0; }; + AST.Program = 'Program'; + AST.ExpressionStatement = 'ExpressionStatement'; + AST.AssignmentExpression = 'AssignmentExpression'; + AST.ConditionalExpression = 'ConditionalExpression'; + AST.LogicalExpression = 'LogicalExpression'; + AST.BinaryExpression = 'BinaryExpression'; + AST.UnaryExpression = 'UnaryExpression'; + AST.CallExpression = 'CallExpression'; + AST.MemberExpression = 'MemberExpression'; + AST.Identifier = 'Identifier'; + AST.Literal = 'Literal'; + AST.ArrayExpression = 'ArrayExpression'; + AST.Property = 'Property'; + AST.ObjectExpression = 'ObjectExpression'; + AST.ThisExpression = 'ThisExpression'; + AST.LocalsExpression = 'LocalsExpression'; - Parser.prototype = { - constructor: Parser, +// Internal use only + AST.NGValueParameter = 'NGValueParameter'; - parse: function (text, json) { + AST.prototype = { + ast: function(text) { this.text = text; - - //TODO(i): strip all the obsolte json stuff from this file - this.json = json; - this.tokens = this.lexer.lex(text); - if (json) { - // The extra level of aliasing is here, just in case the lexer misses something, so that - // we prevent any accidental execution in JSON. - this.assignment = this.logicalOR; - - this.functionCall = - this.fieldAccess = - this.objectIndex = - this.filterChain = function() { - this.throwError('is not valid json', {text: text, index: 0}); - }; - } - - var value = json ? this.primary() : this.statements(); + var value = this.program(); if (this.tokens.length !== 0) { this.throwError('is an unexpected token', this.tokens[0]); } - value.literal = !!value.literal; - value.constant = !!value.constant; - return value; }, - primary: function () { - var primary; - if (this.expect('(')) { - primary = this.filterChain(); - this.consume(')'); - } else if (this.expect('[')) { - primary = this.arrayDeclaration(); - } else if (this.expect('{')) { - primary = this.object(); - } else { - var token = this.expect(); - primary = token.fn; - if (!primary) { - this.throwError('not a primary expression', token); - } - if (token.json) { - primary.constant = true; - primary.literal = true; - } - } - - var next, context; - while ((next = this.expect('(', '[', '.'))) { - if (next.text === '(') { - primary = this.functionCall(primary, context); - context = null; - } else if (next.text === '[') { - context = primary; - primary = this.objectIndex(primary); - } else if (next.text === '.') { - context = primary; - primary = this.fieldAccess(primary); - } else { - this.throwError('IMPOSSIBLE'); - } - } - return primary; - }, - - throwError: function(msg, token) { - throw $parseMinErr('syntax', - 'Syntax Error: Token \'{0}\' {1} at column {2} of the expression [{3}] starting at [{4}].', - token.text, msg, (token.index + 1), this.text, this.text.substring(token.index)); - }, - - peekToken: function() { - if (this.tokens.length === 0) - throw $parseMinErr('ueoe', 'Unexpected end of expression: {0}', this.text); - return this.tokens[0]; - }, - - peek: function(e1, e2, e3, e4) { - if (this.tokens.length > 0) { - var token = this.tokens[0]; - var t = token.text; - if (t === e1 || t === e2 || t === e3 || t === e4 || - (!e1 && !e2 && !e3 && !e4)) { - return token; - } - } - return false; - }, - - expect: function(e1, e2, e3, e4){ - var token = this.peek(e1, e2, e3, e4); - if (token) { - if (this.json && !token.json) { - this.throwError('is not valid json', token); - } - this.tokens.shift(); - return token; - } - return false; - }, - - consume: function(e1){ - if (!this.expect(e1)) { - this.throwError('is unexpected, expecting [' + e1 + ']', this.peek()); - } - }, - - unaryFn: function(fn, right) { - return extend(function(self, locals) { - return fn(self, locals, right); - }, { - constant:right.constant - }); - }, - - ternaryFn: function(left, middle, right){ - return extend(function(self, locals){ - return left(self, locals) ? middle(self, locals) : right(self, locals); - }, { - constant: left.constant && middle.constant && right.constant - }); - }, - - binaryFn: function(left, fn, right) { - return extend(function(self, locals) { - return fn(self, locals, left, right); - }, { - constant:left.constant && right.constant - }); - }, - - statements: function() { - var statements = []; + program: function() { + var body = []; while (true) { if (this.tokens.length > 0 && !this.peek('}', ')', ';', ']')) - statements.push(this.filterChain()); + body.push(this.expressionStatement()); if (!this.expect(';')) { - // optimize for the common case where there is only one statement. - // TODO(size): maybe we should not support multiple statements? - return (statements.length === 1) - ? statements[0] - : function(self, locals) { - var value; - for (var i = 0; i < statements.length; i++) { - var statement = statements[i]; - if (statement) { - value = statement(self, locals); - } - } - return value; - }; + return { type: AST.Program, body: body}; } } }, + expressionStatement: function() { + return { type: AST.ExpressionStatement, expression: this.filterChain() }; + }, + filterChain: function() { var left = this.expression(); - var token; - while (true) { - if ((token = this.expect('|'))) { - left = this.binaryFn(left, token.fn, this.filter()); - } else { - return left; - } - } - }, - - filter: function() { - var token = this.expect(); - var fn = this.$filter(token.text); - var argsFn = []; - while (true) { - if ((token = this.expect(':'))) { - argsFn.push(this.expression()); - } else { - var fnInvoke = function(self, locals, input) { - var args = [input]; - for (var i = 0; i < argsFn.length; i++) { - args.push(argsFn[i](self, locals)); - } - return fn.apply(self, args); - }; - return function() { - return fnInvoke; - }; - } + while (this.expect('|')) { + left = this.filter(left); } + return left; }, expression: function() { @@ -10022,55 +15039,43 @@ }, assignment: function() { - var left = this.ternary(); - var right; - var token; - if ((token = this.expect('='))) { - if (!left.assign) { - this.throwError('implies assignment but [' + - this.text.substring(0, token.index) + '] can not be assigned to', token); + var result = this.ternary(); + if (this.expect('=')) { + if (!isAssignable(result)) { + throw $parseMinErr('lval', 'Trying to assign a value to a non l-value'); } - right = this.ternary(); - return function(scope, locals) { - return left.assign(scope, right(scope, locals), locals); - }; + + result = { type: AST.AssignmentExpression, left: result, right: this.assignment(), operator: '='}; } - return left; + return result; }, ternary: function() { - var left = this.logicalOR(); - var middle; - var token; - if ((token = this.expect('?'))) { - middle = this.ternary(); - if ((token = this.expect(':'))) { - return this.ternaryFn(left, middle, this.ternary()); - } else { - this.throwError('expected :', token); + var test = this.logicalOR(); + var alternate; + var consequent; + if (this.expect('?')) { + alternate = this.expression(); + if (this.consume(':')) { + consequent = this.expression(); + return { type: AST.ConditionalExpression, test: test, alternate: alternate, consequent: consequent}; } - } else { - return left; } + return test; }, logicalOR: function() { var left = this.logicalAND(); - var token; - while (true) { - if ((token = this.expect('||'))) { - left = this.binaryFn(left, token.fn, this.logicalAND()); - } else { - return left; - } + while (this.expect('||')) { + left = { type: AST.LogicalExpression, operator: '||', left: left, right: this.logicalAND() }; } + return left; }, logicalAND: function() { var left = this.equality(); - var token; - if ((token = this.expect('&&'))) { - left = this.binaryFn(left, token.fn, this.logicalAND()); + while (this.expect('&&')) { + left = { type: AST.LogicalExpression, operator: '&&', left: left, right: this.equality()}; } return left; }, @@ -10078,8 +15083,8 @@ equality: function() { var left = this.relational(); var token; - if ((token = this.expect('==','!=','===','!=='))) { - left = this.binaryFn(left, token.fn, this.equality()); + while ((token = this.expect('==','!=','===','!=='))) { + left = { type: AST.BinaryExpression, operator: token.text, left: left, right: this.relational() }; } return left; }, @@ -10087,8 +15092,8 @@ relational: function() { var left = this.additive(); var token; - if ((token = this.expect('<', '>', '<=', '>='))) { - left = this.binaryFn(left, token.fn, this.relational()); + while ((token = this.expect('<', '>', '<=', '>='))) { + left = { type: AST.BinaryExpression, operator: token.text, left: left, right: this.additive() }; } return left; }, @@ -10097,7 +15102,7 @@ var left = this.multiplicative(); var token; while ((token = this.expect('+','-'))) { - left = this.binaryFn(left, token.fn, this.multiplicative()); + left = { type: AST.BinaryExpression, operator: token.text, left: left, right: this.multiplicative() }; } return left; }, @@ -10106,415 +15111,1266 @@ var left = this.unary(); var token; while ((token = this.expect('*','/','%'))) { - left = this.binaryFn(left, token.fn, this.unary()); + left = { type: AST.BinaryExpression, operator: token.text, left: left, right: this.unary() }; } return left; }, unary: function() { var token; - if (this.expect('+')) { - return this.primary(); - } else if ((token = this.expect('-'))) { - return this.binaryFn(Parser.ZERO, token.fn, this.unary()); - } else if ((token = this.expect('!'))) { - return this.unaryFn(token.fn, this.unary()); + if ((token = this.expect('+', '-', '!'))) { + return { type: AST.UnaryExpression, operator: token.text, prefix: true, argument: this.unary() }; } else { return this.primary(); } }, - fieldAccess: function(object) { - var parser = this; - var field = this.expect().text; - var getter = getterFn(field, this.options, this.text); + primary: function() { + var primary; + if (this.expect('(')) { + primary = this.filterChain(); + this.consume(')'); + } else if (this.expect('[')) { + primary = this.arrayDeclaration(); + } else if (this.expect('{')) { + primary = this.object(); + } else if (this.selfReferential.hasOwnProperty(this.peek().text)) { + primary = copy(this.selfReferential[this.consume().text]); + } else if (this.options.literals.hasOwnProperty(this.peek().text)) { + primary = { type: AST.Literal, value: this.options.literals[this.consume().text]}; + } else if (this.peek().identifier) { + primary = this.identifier(); + } else if (this.peek().constant) { + primary = this.constant(); + } else { + this.throwError('not a primary expression', this.peek()); + } - return extend(function(scope, locals, self) { - return getter(self || object(scope, locals), locals); - }, { - assign: function(scope, value, locals) { - return setter(object(scope, locals), field, value, parser.text, parser.options); + var next; + while ((next = this.expect('(', '[', '.'))) { + if (next.text === '(') { + primary = {type: AST.CallExpression, callee: primary, arguments: this.parseArguments() }; + this.consume(')'); + } else if (next.text === '[') { + primary = { type: AST.MemberExpression, object: primary, property: this.expression(), computed: true }; + this.consume(']'); + } else if (next.text === '.') { + primary = { type: AST.MemberExpression, object: primary, property: this.identifier(), computed: false }; + } else { + this.throwError('IMPOSSIBLE'); } - }); + } + return primary; }, - objectIndex: function(obj) { - var parser = this; + filter: function(baseExpression) { + var args = [baseExpression]; + var result = {type: AST.CallExpression, callee: this.identifier(), arguments: args, filter: true}; - var indexFn = this.expression(); - this.consume(']'); + while (this.expect(':')) { + args.push(this.expression()); + } - return extend(function(self, locals) { - var o = obj(self, locals), - i = indexFn(self, locals), - v, p; - - if (!o) return undefined; - v = ensureSafeObject(o[i], parser.text); - if (v && v.then && parser.options.unwrapPromises) { - p = v; - if (!('$$v' in v)) { - p.$$v = undefined; - p.then(function(val) { p.$$v = val; }); - } - v = v.$$v; - } - return v; - }, { - assign: function(self, value, locals) { - var key = indexFn(self, locals); - // prevent overwriting of Function.constructor which would break ensureSafeObject check - var safe = ensureSafeObject(obj(self, locals), parser.text); - return safe[key] = value; - } - }); + return result; }, - functionCall: function(fn, contextGetter) { - var argsFn = []; + parseArguments: function() { + var args = []; if (this.peekToken().text !== ')') { do { - argsFn.push(this.expression()); + args.push(this.filterChain()); } while (this.expect(',')); } - this.consume(')'); - - var parser = this; - - return function(scope, locals) { - var args = []; - var context = contextGetter ? contextGetter(scope, locals) : scope; - - for (var i = 0; i < argsFn.length; i++) { - args.push(argsFn[i](scope, locals)); - } - var fnPtr = fn(scope, locals, context) || noop; - - ensureSafeObject(context, parser.text); - ensureSafeObject(fnPtr, parser.text); - - // IE stupidity! (IE doesn't have apply for some native functions) - var v = fnPtr.apply - ? fnPtr.apply(context, args) - : fnPtr(args[0], args[1], args[2], args[3], args[4]); - - return ensureSafeObject(v, parser.text); - }; + return args; }, - // This is used with json array declaration - arrayDeclaration: function () { - var elementFns = []; - var allConstant = true; + identifier: function() { + var token = this.consume(); + if (!token.identifier) { + this.throwError('is not a valid identifier', token); + } + return { type: AST.Identifier, name: token.text }; + }, + + constant: function() { + // TODO check that it is a constant + return { type: AST.Literal, value: this.consume().value }; + }, + + arrayDeclaration: function() { + var elements = []; if (this.peekToken().text !== ']') { do { - var elementFn = this.expression(); - elementFns.push(elementFn); - if (!elementFn.constant) { - allConstant = false; + if (this.peek(']')) { + // Support trailing commas per ES5.1. + break; } + elements.push(this.expression()); } while (this.expect(',')); } this.consume(']'); - return extend(function(self, locals) { - var array = []; - for (var i = 0; i < elementFns.length; i++) { - array.push(elementFns[i](self, locals)); - } - return array; - }, { - literal: true, - constant: allConstant - }); + return { type: AST.ArrayExpression, elements: elements }; }, - object: function () { - var keyValues = []; - var allConstant = true; + object: function() { + var properties = [], property; if (this.peekToken().text !== '}') { do { - var token = this.expect(), - key = token.string || token.text; - this.consume(':'); - var value = this.expression(); - keyValues.push({key: key, value: value}); - if (!value.constant) { - allConstant = false; + if (this.peek('}')) { + // Support trailing commas per ES5.1. + break; } + property = {type: AST.Property, kind: 'init'}; + if (this.peek().constant) { + property.key = this.constant(); + property.computed = false; + this.consume(':'); + property.value = this.expression(); + } else if (this.peek().identifier) { + property.key = this.identifier(); + property.computed = false; + if (this.peek(':')) { + this.consume(':'); + property.value = this.expression(); + } else { + property.value = property.key; + } + } else if (this.peek('[')) { + this.consume('['); + property.key = this.expression(); + this.consume(']'); + property.computed = true; + this.consume(':'); + property.value = this.expression(); + } else { + this.throwError('invalid key', this.peek()); + } + properties.push(property); } while (this.expect(',')); } this.consume('}'); - return extend(function(self, locals) { - var object = {}; - for (var i = 0; i < keyValues.length; i++) { - var keyValue = keyValues[i]; - object[keyValue.key] = keyValue.value(self, locals); + return {type: AST.ObjectExpression, properties: properties }; + }, + + throwError: function(msg, token) { + throw $parseMinErr('syntax', + 'Syntax Error: Token \'{0}\' {1} at column {2} of the expression [{3}] starting at [{4}].', + token.text, msg, (token.index + 1), this.text, this.text.substring(token.index)); + }, + + consume: function(e1) { + if (this.tokens.length === 0) { + throw $parseMinErr('ueoe', 'Unexpected end of expression: {0}', this.text); + } + + var token = this.expect(e1); + if (!token) { + this.throwError('is unexpected, expecting [' + e1 + ']', this.peek()); + } + return token; + }, + + peekToken: function() { + if (this.tokens.length === 0) { + throw $parseMinErr('ueoe', 'Unexpected end of expression: {0}', this.text); + } + return this.tokens[0]; + }, + + peek: function(e1, e2, e3, e4) { + return this.peekAhead(0, e1, e2, e3, e4); + }, + + peekAhead: function(i, e1, e2, e3, e4) { + if (this.tokens.length > i) { + var token = this.tokens[i]; + var t = token.text; + if (t === e1 || t === e2 || t === e3 || t === e4 || + (!e1 && !e2 && !e3 && !e4)) { + return token; } - return object; - }, { - literal: true, - constant: allConstant + } + return false; + }, + + expect: function(e1, e2, e3, e4) { + var token = this.peek(e1, e2, e3, e4); + if (token) { + this.tokens.shift(); + return token; + } + return false; + }, + + selfReferential: { + 'this': {type: AST.ThisExpression }, + '$locals': {type: AST.LocalsExpression } + } + }; + + function ifDefined(v, d) { + return typeof v !== 'undefined' ? v : d; + } + + function plusFn(l, r) { + if (typeof l === 'undefined') return r; + if (typeof r === 'undefined') return l; + return l + r; + } + + function isStateless($filter, filterName) { + var fn = $filter(filterName); + return !fn.$stateful; + } + + var PURITY_ABSOLUTE = 1; + var PURITY_RELATIVE = 2; + +// Detect nodes which could depend on non-shallow state of objects + function isPure(node, parentIsPure) { + switch (node.type) { + // Computed members might invoke a stateful toString() + case AST.MemberExpression: + if (node.computed) { + return false; + } + break; + + // Unary always convert to primative + case AST.UnaryExpression: + return PURITY_ABSOLUTE; + + // The binary + operator can invoke a stateful toString(). + case AST.BinaryExpression: + return node.operator !== '+' ? PURITY_ABSOLUTE : false; + + // Functions / filters probably read state from within objects + case AST.CallExpression: + return false; + } + + return (undefined === parentIsPure) ? PURITY_RELATIVE : parentIsPure; + } + + function findConstantAndWatchExpressions(ast, $filter, parentIsPure) { + var allConstants; + var argsToWatch; + var isStatelessFilter; + + var astIsPure = ast.isPure = isPure(ast, parentIsPure); + + switch (ast.type) { + case AST.Program: + allConstants = true; + forEach(ast.body, function(expr) { + findConstantAndWatchExpressions(expr.expression, $filter, astIsPure); + allConstants = allConstants && expr.expression.constant; + }); + ast.constant = allConstants; + break; + case AST.Literal: + ast.constant = true; + ast.toWatch = []; + break; + case AST.UnaryExpression: + findConstantAndWatchExpressions(ast.argument, $filter, astIsPure); + ast.constant = ast.argument.constant; + ast.toWatch = ast.argument.toWatch; + break; + case AST.BinaryExpression: + findConstantAndWatchExpressions(ast.left, $filter, astIsPure); + findConstantAndWatchExpressions(ast.right, $filter, astIsPure); + ast.constant = ast.left.constant && ast.right.constant; + ast.toWatch = ast.left.toWatch.concat(ast.right.toWatch); + break; + case AST.LogicalExpression: + findConstantAndWatchExpressions(ast.left, $filter, astIsPure); + findConstantAndWatchExpressions(ast.right, $filter, astIsPure); + ast.constant = ast.left.constant && ast.right.constant; + ast.toWatch = ast.constant ? [] : [ast]; + break; + case AST.ConditionalExpression: + findConstantAndWatchExpressions(ast.test, $filter, astIsPure); + findConstantAndWatchExpressions(ast.alternate, $filter, astIsPure); + findConstantAndWatchExpressions(ast.consequent, $filter, astIsPure); + ast.constant = ast.test.constant && ast.alternate.constant && ast.consequent.constant; + ast.toWatch = ast.constant ? [] : [ast]; + break; + case AST.Identifier: + ast.constant = false; + ast.toWatch = [ast]; + break; + case AST.MemberExpression: + findConstantAndWatchExpressions(ast.object, $filter, astIsPure); + if (ast.computed) { + findConstantAndWatchExpressions(ast.property, $filter, astIsPure); + } + ast.constant = ast.object.constant && (!ast.computed || ast.property.constant); + ast.toWatch = ast.constant ? [] : [ast]; + break; + case AST.CallExpression: + isStatelessFilter = ast.filter ? isStateless($filter, ast.callee.name) : false; + allConstants = isStatelessFilter; + argsToWatch = []; + forEach(ast.arguments, function(expr) { + findConstantAndWatchExpressions(expr, $filter, astIsPure); + allConstants = allConstants && expr.constant; + argsToWatch.push.apply(argsToWatch, expr.toWatch); + }); + ast.constant = allConstants; + ast.toWatch = isStatelessFilter ? argsToWatch : [ast]; + break; + case AST.AssignmentExpression: + findConstantAndWatchExpressions(ast.left, $filter, astIsPure); + findConstantAndWatchExpressions(ast.right, $filter, astIsPure); + ast.constant = ast.left.constant && ast.right.constant; + ast.toWatch = [ast]; + break; + case AST.ArrayExpression: + allConstants = true; + argsToWatch = []; + forEach(ast.elements, function(expr) { + findConstantAndWatchExpressions(expr, $filter, astIsPure); + allConstants = allConstants && expr.constant; + argsToWatch.push.apply(argsToWatch, expr.toWatch); + }); + ast.constant = allConstants; + ast.toWatch = argsToWatch; + break; + case AST.ObjectExpression: + allConstants = true; + argsToWatch = []; + forEach(ast.properties, function(property) { + findConstantAndWatchExpressions(property.value, $filter, astIsPure); + allConstants = allConstants && property.value.constant; + argsToWatch.push.apply(argsToWatch, property.value.toWatch); + if (property.computed) { + //`{[key]: value}` implicitly does `key.toString()` which may be non-pure + findConstantAndWatchExpressions(property.key, $filter, /*parentIsPure=*/false); + allConstants = allConstants && property.key.constant; + argsToWatch.push.apply(argsToWatch, property.key.toWatch); + } + }); + ast.constant = allConstants; + ast.toWatch = argsToWatch; + break; + case AST.ThisExpression: + ast.constant = false; + ast.toWatch = []; + break; + case AST.LocalsExpression: + ast.constant = false; + ast.toWatch = []; + break; + } + } + + function getInputs(body) { + if (body.length !== 1) return; + var lastExpression = body[0].expression; + var candidate = lastExpression.toWatch; + if (candidate.length !== 1) return candidate; + return candidate[0] !== lastExpression ? candidate : undefined; + } + + function isAssignable(ast) { + return ast.type === AST.Identifier || ast.type === AST.MemberExpression; + } + + function assignableAST(ast) { + if (ast.body.length === 1 && isAssignable(ast.body[0].expression)) { + return {type: AST.AssignmentExpression, left: ast.body[0].expression, right: {type: AST.NGValueParameter}, operator: '='}; + } + } + + function isLiteral(ast) { + return ast.body.length === 0 || + ast.body.length === 1 && ( + ast.body[0].expression.type === AST.Literal || + ast.body[0].expression.type === AST.ArrayExpression || + ast.body[0].expression.type === AST.ObjectExpression); + } + + function isConstant(ast) { + return ast.constant; + } + + function ASTCompiler($filter) { + this.$filter = $filter; + } + + ASTCompiler.prototype = { + compile: function(ast) { + var self = this; + this.state = { + nextId: 0, + filters: {}, + fn: {vars: [], body: [], own: {}}, + assign: {vars: [], body: [], own: {}}, + inputs: [] + }; + findConstantAndWatchExpressions(ast, self.$filter); + var extra = ''; + var assignable; + this.stage = 'assign'; + if ((assignable = assignableAST(ast))) { + this.state.computing = 'assign'; + var result = this.nextId(); + this.recurse(assignable, result); + this.return_(result); + extra = 'fn.assign=' + this.generateFunction('assign', 's,v,l'); + } + var toWatch = getInputs(ast.body); + self.stage = 'inputs'; + forEach(toWatch, function(watch, key) { + var fnKey = 'fn' + key; + self.state[fnKey] = {vars: [], body: [], own: {}}; + self.state.computing = fnKey; + var intoId = self.nextId(); + self.recurse(watch, intoId); + self.return_(intoId); + self.state.inputs.push({name: fnKey, isPure: watch.isPure}); + watch.watchId = key; }); + this.state.computing = 'fn'; + this.stage = 'main'; + this.recurse(ast); + var fnString = + // The build and minification steps remove the string "use strict" from the code, but this is done using a regex. + // This is a workaround for this until we do a better job at only removing the prefix only when we should. + '"' + this.USE + ' ' + this.STRICT + '";\n' + + this.filterPrefix() + + 'var fn=' + this.generateFunction('fn', 's,l,a,i') + + extra + + this.watchFns() + + 'return fn;'; + + // eslint-disable-next-line no-new-func + var fn = (new Function('$filter', + 'getStringValue', + 'ifDefined', + 'plus', + fnString))( + this.$filter, + getStringValue, + ifDefined, + plusFn); + this.state = this.stage = undefined; + return fn; + }, + + USE: 'use', + + STRICT: 'strict', + + watchFns: function() { + var result = []; + var inputs = this.state.inputs; + var self = this; + forEach(inputs, function(input) { + result.push('var ' + input.name + '=' + self.generateFunction(input.name, 's')); + if (input.isPure) { + result.push(input.name, '.isPure=' + JSON.stringify(input.isPure) + ';'); + } + }); + if (inputs.length) { + result.push('fn.inputs=[' + inputs.map(function(i) { return i.name; }).join(',') + '];'); + } + return result.join(''); + }, + + generateFunction: function(name, params) { + return 'function(' + params + '){' + + this.varsPrefix(name) + + this.body(name) + + '};'; + }, + + filterPrefix: function() { + var parts = []; + var self = this; + forEach(this.state.filters, function(id, filter) { + parts.push(id + '=$filter(' + self.escape(filter) + ')'); + }); + if (parts.length) return 'var ' + parts.join(',') + ';'; + return ''; + }, + + varsPrefix: function(section) { + return this.state[section].vars.length ? 'var ' + this.state[section].vars.join(',') + ';' : ''; + }, + + body: function(section) { + return this.state[section].body.join(''); + }, + + recurse: function(ast, intoId, nameId, recursionFn, create, skipWatchIdCheck) { + var left, right, self = this, args, expression, computed; + recursionFn = recursionFn || noop; + if (!skipWatchIdCheck && isDefined(ast.watchId)) { + intoId = intoId || this.nextId(); + this.if_('i', + this.lazyAssign(intoId, this.computedMember('i', ast.watchId)), + this.lazyRecurse(ast, intoId, nameId, recursionFn, create, true) + ); + return; + } + switch (ast.type) { + case AST.Program: + forEach(ast.body, function(expression, pos) { + self.recurse(expression.expression, undefined, undefined, function(expr) { right = expr; }); + if (pos !== ast.body.length - 1) { + self.current().body.push(right, ';'); + } else { + self.return_(right); + } + }); + break; + case AST.Literal: + expression = this.escape(ast.value); + this.assign(intoId, expression); + recursionFn(intoId || expression); + break; + case AST.UnaryExpression: + this.recurse(ast.argument, undefined, undefined, function(expr) { right = expr; }); + expression = ast.operator + '(' + this.ifDefined(right, 0) + ')'; + this.assign(intoId, expression); + recursionFn(expression); + break; + case AST.BinaryExpression: + this.recurse(ast.left, undefined, undefined, function(expr) { left = expr; }); + this.recurse(ast.right, undefined, undefined, function(expr) { right = expr; }); + if (ast.operator === '+') { + expression = this.plus(left, right); + } else if (ast.operator === '-') { + expression = this.ifDefined(left, 0) + ast.operator + this.ifDefined(right, 0); + } else { + expression = '(' + left + ')' + ast.operator + '(' + right + ')'; + } + this.assign(intoId, expression); + recursionFn(expression); + break; + case AST.LogicalExpression: + intoId = intoId || this.nextId(); + self.recurse(ast.left, intoId); + self.if_(ast.operator === '&&' ? intoId : self.not(intoId), self.lazyRecurse(ast.right, intoId)); + recursionFn(intoId); + break; + case AST.ConditionalExpression: + intoId = intoId || this.nextId(); + self.recurse(ast.test, intoId); + self.if_(intoId, self.lazyRecurse(ast.alternate, intoId), self.lazyRecurse(ast.consequent, intoId)); + recursionFn(intoId); + break; + case AST.Identifier: + intoId = intoId || this.nextId(); + if (nameId) { + nameId.context = self.stage === 'inputs' ? 's' : this.assign(this.nextId(), this.getHasOwnProperty('l', ast.name) + '?l:s'); + nameId.computed = false; + nameId.name = ast.name; + } + self.if_(self.stage === 'inputs' || self.not(self.getHasOwnProperty('l', ast.name)), + function() { + self.if_(self.stage === 'inputs' || 's', function() { + if (create && create !== 1) { + self.if_( + self.isNull(self.nonComputedMember('s', ast.name)), + self.lazyAssign(self.nonComputedMember('s', ast.name), '{}')); + } + self.assign(intoId, self.nonComputedMember('s', ast.name)); + }); + }, intoId && self.lazyAssign(intoId, self.nonComputedMember('l', ast.name)) + ); + recursionFn(intoId); + break; + case AST.MemberExpression: + left = nameId && (nameId.context = this.nextId()) || this.nextId(); + intoId = intoId || this.nextId(); + self.recurse(ast.object, left, undefined, function() { + self.if_(self.notNull(left), function() { + if (ast.computed) { + right = self.nextId(); + self.recurse(ast.property, right); + self.getStringValue(right); + if (create && create !== 1) { + self.if_(self.not(self.computedMember(left, right)), self.lazyAssign(self.computedMember(left, right), '{}')); + } + expression = self.computedMember(left, right); + self.assign(intoId, expression); + if (nameId) { + nameId.computed = true; + nameId.name = right; + } + } else { + if (create && create !== 1) { + self.if_(self.isNull(self.nonComputedMember(left, ast.property.name)), self.lazyAssign(self.nonComputedMember(left, ast.property.name), '{}')); + } + expression = self.nonComputedMember(left, ast.property.name); + self.assign(intoId, expression); + if (nameId) { + nameId.computed = false; + nameId.name = ast.property.name; + } + } + }, function() { + self.assign(intoId, 'undefined'); + }); + recursionFn(intoId); + }, !!create); + break; + case AST.CallExpression: + intoId = intoId || this.nextId(); + if (ast.filter) { + right = self.filter(ast.callee.name); + args = []; + forEach(ast.arguments, function(expr) { + var argument = self.nextId(); + self.recurse(expr, argument); + args.push(argument); + }); + expression = right + '(' + args.join(',') + ')'; + self.assign(intoId, expression); + recursionFn(intoId); + } else { + right = self.nextId(); + left = {}; + args = []; + self.recurse(ast.callee, right, left, function() { + self.if_(self.notNull(right), function() { + forEach(ast.arguments, function(expr) { + self.recurse(expr, ast.constant ? undefined : self.nextId(), undefined, function(argument) { + args.push(argument); + }); + }); + if (left.name) { + expression = self.member(left.context, left.name, left.computed) + '(' + args.join(',') + ')'; + } else { + expression = right + '(' + args.join(',') + ')'; + } + self.assign(intoId, expression); + }, function() { + self.assign(intoId, 'undefined'); + }); + recursionFn(intoId); + }); + } + break; + case AST.AssignmentExpression: + right = this.nextId(); + left = {}; + this.recurse(ast.left, undefined, left, function() { + self.if_(self.notNull(left.context), function() { + self.recurse(ast.right, right); + expression = self.member(left.context, left.name, left.computed) + ast.operator + right; + self.assign(intoId, expression); + recursionFn(intoId || expression); + }); + }, 1); + break; + case AST.ArrayExpression: + args = []; + forEach(ast.elements, function(expr) { + self.recurse(expr, ast.constant ? undefined : self.nextId(), undefined, function(argument) { + args.push(argument); + }); + }); + expression = '[' + args.join(',') + ']'; + this.assign(intoId, expression); + recursionFn(intoId || expression); + break; + case AST.ObjectExpression: + args = []; + computed = false; + forEach(ast.properties, function(property) { + if (property.computed) { + computed = true; + } + }); + if (computed) { + intoId = intoId || this.nextId(); + this.assign(intoId, '{}'); + forEach(ast.properties, function(property) { + if (property.computed) { + left = self.nextId(); + self.recurse(property.key, left); + } else { + left = property.key.type === AST.Identifier ? + property.key.name : + ('' + property.key.value); + } + right = self.nextId(); + self.recurse(property.value, right); + self.assign(self.member(intoId, left, property.computed), right); + }); + } else { + forEach(ast.properties, function(property) { + self.recurse(property.value, ast.constant ? undefined : self.nextId(), undefined, function(expr) { + args.push(self.escape( + property.key.type === AST.Identifier ? property.key.name : + ('' + property.key.value)) + + ':' + expr); + }); + }); + expression = '{' + args.join(',') + '}'; + this.assign(intoId, expression); + } + recursionFn(intoId || expression); + break; + case AST.ThisExpression: + this.assign(intoId, 's'); + recursionFn(intoId || 's'); + break; + case AST.LocalsExpression: + this.assign(intoId, 'l'); + recursionFn(intoId || 'l'); + break; + case AST.NGValueParameter: + this.assign(intoId, 'v'); + recursionFn(intoId || 'v'); + break; + } + }, + + getHasOwnProperty: function(element, property) { + var key = element + '.' + property; + var own = this.current().own; + if (!own.hasOwnProperty(key)) { + own[key] = this.nextId(false, element + '&&(' + this.escape(property) + ' in ' + element + ')'); + } + return own[key]; + }, + + assign: function(id, value) { + if (!id) return; + this.current().body.push(id, '=', value, ';'); + return id; + }, + + filter: function(filterName) { + if (!this.state.filters.hasOwnProperty(filterName)) { + this.state.filters[filterName] = this.nextId(true); + } + return this.state.filters[filterName]; + }, + + ifDefined: function(id, defaultValue) { + return 'ifDefined(' + id + ',' + this.escape(defaultValue) + ')'; + }, + + plus: function(left, right) { + return 'plus(' + left + ',' + right + ')'; + }, + + return_: function(id) { + this.current().body.push('return ', id, ';'); + }, + + if_: function(test, alternate, consequent) { + if (test === true) { + alternate(); + } else { + var body = this.current().body; + body.push('if(', test, '){'); + alternate(); + body.push('}'); + if (consequent) { + body.push('else{'); + consequent(); + body.push('}'); + } + } + }, + + not: function(expression) { + return '!(' + expression + ')'; + }, + + isNull: function(expression) { + return expression + '==null'; + }, + + notNull: function(expression) { + return expression + '!=null'; + }, + + nonComputedMember: function(left, right) { + var SAFE_IDENTIFIER = /^[$_a-zA-Z][$_a-zA-Z0-9]*$/; + var UNSAFE_CHARACTERS = /[^$_a-zA-Z0-9]/g; + if (SAFE_IDENTIFIER.test(right)) { + return left + '.' + right; + } else { + return left + '["' + right.replace(UNSAFE_CHARACTERS, this.stringEscapeFn) + '"]'; + } + }, + + computedMember: function(left, right) { + return left + '[' + right + ']'; + }, + + member: function(left, right, computed) { + if (computed) return this.computedMember(left, right); + return this.nonComputedMember(left, right); + }, + + getStringValue: function(item) { + this.assign(item, 'getStringValue(' + item + ')'); + }, + + lazyRecurse: function(ast, intoId, nameId, recursionFn, create, skipWatchIdCheck) { + var self = this; + return function() { + self.recurse(ast, intoId, nameId, recursionFn, create, skipWatchIdCheck); + }; + }, + + lazyAssign: function(id, value) { + var self = this; + return function() { + self.assign(id, value); + }; + }, + + stringEscapeRegex: /[^ a-zA-Z0-9]/g, + + stringEscapeFn: function(c) { + return '\\u' + ('0000' + c.charCodeAt(0).toString(16)).slice(-4); + }, + + escape: function(value) { + if (isString(value)) return '\'' + value.replace(this.stringEscapeRegex, this.stringEscapeFn) + '\''; + if (isNumber(value)) return value.toString(); + if (value === true) return 'true'; + if (value === false) return 'false'; + if (value === null) return 'null'; + if (typeof value === 'undefined') return 'undefined'; + + throw $parseMinErr('esc', 'IMPOSSIBLE'); + }, + + nextId: function(skip, init) { + var id = 'v' + (this.state.nextId++); + if (!skip) { + this.current().vars.push(id + (init ? '=' + init : '')); + } + return id; + }, + + current: function() { + return this.state[this.state.computing]; } }; -////////////////////////////////////////////////// -// Parser helper functions -////////////////////////////////////////////////// - - function setter(obj, path, setValue, fullExp, options) { - //needed? - options = options || {}; - - var element = path.split('.'), key; - for (var i = 0; element.length > 1; i++) { - key = ensureSafeMemberName(element.shift(), fullExp); - var propertyObj = obj[key]; - if (!propertyObj) { - propertyObj = {}; - obj[key] = propertyObj; - } - obj = propertyObj; - if (obj.then && options.unwrapPromises) { - promiseWarning(fullExp); - if (!("$$v" in obj)) { - (function(promise) { - promise.then(function(val) { promise.$$v = val; }); } - )(obj); - } - if (obj.$$v === undefined) { - obj.$$v = {}; - } - obj = obj.$$v; - } - } - key = ensureSafeMemberName(element.shift(), fullExp); - obj[key] = setValue; - return setValue; + function ASTInterpreter($filter) { + this.$filter = $filter; } - var getterFnCache = {}; - - /** - * Implementation of the "Black Hole" variant from: - * - http://jsperf.com/angularjs-parse-getter/4 - * - http://jsperf.com/path-evaluation-simplified/7 - */ - function cspSafeGetterFn(key0, key1, key2, key3, key4, fullExp, options) { - ensureSafeMemberName(key0, fullExp); - ensureSafeMemberName(key1, fullExp); - ensureSafeMemberName(key2, fullExp); - ensureSafeMemberName(key3, fullExp); - ensureSafeMemberName(key4, fullExp); - - return !options.unwrapPromises - ? function cspSafeGetter(scope, locals) { - var pathVal = (locals && locals.hasOwnProperty(key0)) ? locals : scope; - - if (pathVal == null) return pathVal; - pathVal = pathVal[key0]; - - if (!key1) return pathVal; - if (pathVal == null) return undefined; - pathVal = pathVal[key1]; - - if (!key2) return pathVal; - if (pathVal == null) return undefined; - pathVal = pathVal[key2]; - - if (!key3) return pathVal; - if (pathVal == null) return undefined; - pathVal = pathVal[key3]; - - if (!key4) return pathVal; - if (pathVal == null) return undefined; - pathVal = pathVal[key4]; - - return pathVal; - } - : function cspSafePromiseEnabledGetter(scope, locals) { - var pathVal = (locals && locals.hasOwnProperty(key0)) ? locals : scope, - promise; - - if (pathVal == null) return pathVal; - - pathVal = pathVal[key0]; - if (pathVal && pathVal.then) { - promiseWarning(fullExp); - if (!("$$v" in pathVal)) { - promise = pathVal; - promise.$$v = undefined; - promise.then(function(val) { promise.$$v = val; }); - } - pathVal = pathVal.$$v; + ASTInterpreter.prototype = { + compile: function(ast) { + var self = this; + findConstantAndWatchExpressions(ast, self.$filter); + var assignable; + var assign; + if ((assignable = assignableAST(ast))) { + assign = this.recurse(assignable); } - - if (!key1) return pathVal; - if (pathVal == null) return undefined; - pathVal = pathVal[key1]; - if (pathVal && pathVal.then) { - promiseWarning(fullExp); - if (!("$$v" in pathVal)) { - promise = pathVal; - promise.$$v = undefined; - promise.then(function(val) { promise.$$v = val; }); - } - pathVal = pathVal.$$v; + var toWatch = getInputs(ast.body); + var inputs; + if (toWatch) { + inputs = []; + forEach(toWatch, function(watch, key) { + var input = self.recurse(watch); + input.isPure = watch.isPure; + watch.input = input; + inputs.push(input); + watch.watchId = key; + }); } - - if (!key2) return pathVal; - if (pathVal == null) return undefined; - pathVal = pathVal[key2]; - if (pathVal && pathVal.then) { - promiseWarning(fullExp); - if (!("$$v" in pathVal)) { - promise = pathVal; - promise.$$v = undefined; - promise.then(function(val) { promise.$$v = val; }); - } - pathVal = pathVal.$$v; - } - - if (!key3) return pathVal; - if (pathVal == null) return undefined; - pathVal = pathVal[key3]; - if (pathVal && pathVal.then) { - promiseWarning(fullExp); - if (!("$$v" in pathVal)) { - promise = pathVal; - promise.$$v = undefined; - promise.then(function(val) { promise.$$v = val; }); - } - pathVal = pathVal.$$v; - } - - if (!key4) return pathVal; - if (pathVal == null) return undefined; - pathVal = pathVal[key4]; - if (pathVal && pathVal.then) { - promiseWarning(fullExp); - if (!("$$v" in pathVal)) { - promise = pathVal; - promise.$$v = undefined; - promise.then(function(val) { promise.$$v = val; }); - } - pathVal = pathVal.$$v; - } - return pathVal; - }; - } - - function simpleGetterFn1(key0, fullExp) { - ensureSafeMemberName(key0, fullExp); - - return function simpleGetterFn1(scope, locals) { - if (scope == null) return undefined; - return ((locals && locals.hasOwnProperty(key0)) ? locals : scope)[key0]; - }; - } - - function simpleGetterFn2(key0, key1, fullExp) { - ensureSafeMemberName(key0, fullExp); - ensureSafeMemberName(key1, fullExp); - - return function simpleGetterFn2(scope, locals) { - if (scope == null) return undefined; - scope = ((locals && locals.hasOwnProperty(key0)) ? locals : scope)[key0]; - return scope == null ? undefined : scope[key1]; - }; - } - - function getterFn(path, options, fullExp) { - // Check whether the cache has this getter already. - // We can use hasOwnProperty directly on the cache because we ensure, - // see below, that the cache never stores a path called 'hasOwnProperty' - if (getterFnCache.hasOwnProperty(path)) { - return getterFnCache[path]; - } - - var pathKeys = path.split('.'), - pathKeysLength = pathKeys.length, - fn; - - // When we have only 1 or 2 tokens, use optimized special case closures. - // http://jsperf.com/angularjs-parse-getter/6 - if (!options.unwrapPromises && pathKeysLength === 1) { - fn = simpleGetterFn1(pathKeys[0], fullExp); - } else if (!options.unwrapPromises && pathKeysLength === 2) { - fn = simpleGetterFn2(pathKeys[0], pathKeys[1], fullExp); - } else if (options.csp) { - if (pathKeysLength < 6) { - fn = cspSafeGetterFn(pathKeys[0], pathKeys[1], pathKeys[2], pathKeys[3], pathKeys[4], fullExp, - options); - } else { - fn = function(scope, locals) { - var i = 0, val; - do { - val = cspSafeGetterFn(pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++], - pathKeys[i++], fullExp, options)(scope, locals); - - locals = undefined; // clear after first iteration - scope = val; - } while (i < pathKeysLength); - return val; + var expressions = []; + forEach(ast.body, function(expression) { + expressions.push(self.recurse(expression.expression)); + }); + var fn = ast.body.length === 0 ? noop : + ast.body.length === 1 ? expressions[0] : + function(scope, locals) { + var lastValue; + forEach(expressions, function(exp) { + lastValue = exp(scope, locals); + }); + return lastValue; + }; + if (assign) { + fn.assign = function(scope, value, locals) { + return assign(scope, locals, value); }; } - } else { - var code = 'var p;\n'; - forEach(pathKeys, function(key, index) { - ensureSafeMemberName(key, fullExp); - code += 'if(s == null) return undefined;\n' + - 's='+ (index - // we simply dereference 's' on any .dot notation - ? 's' - // but if we are first then we check locals first, and if so read it first - : '((k&&k.hasOwnProperty("' + key + '"))?k:s)') + '["' + key + '"]' + ';\n' + - (options.unwrapPromises - ? 'if (s && s.then) {\n' + - ' pw("' + fullExp.replace(/(["\r\n])/g, '\\$1') + '");\n' + - ' if (!("$$v" in s)) {\n' + - ' p=s;\n' + - ' p.$$v = undefined;\n' + - ' p.then(function(v) {p.$$v=v;});\n' + - '}\n' + - ' s=s.$$v\n' + - '}\n' - : ''); - }); - code += 'return s;'; + if (inputs) { + fn.inputs = inputs; + } + return fn; + }, - /* jshint -W054 */ - var evaledFnGetter = new Function('s', 'k', 'pw', code); // s=scope, k=locals, pw=promiseWarning - /* jshint +W054 */ - evaledFnGetter.toString = valueFn(code); - fn = options.unwrapPromises ? function(scope, locals) { - return evaledFnGetter(scope, locals, promiseWarning); - } : evaledFnGetter; - } + recurse: function(ast, context, create) { + var left, right, self = this, args; + if (ast.input) { + return this.inputs(ast.input, ast.watchId); + } + switch (ast.type) { + case AST.Literal: + return this.value(ast.value, context); + case AST.UnaryExpression: + right = this.recurse(ast.argument); + return this['unary' + ast.operator](right, context); + case AST.BinaryExpression: + left = this.recurse(ast.left); + right = this.recurse(ast.right); + return this['binary' + ast.operator](left, right, context); + case AST.LogicalExpression: + left = this.recurse(ast.left); + right = this.recurse(ast.right); + return this['binary' + ast.operator](left, right, context); + case AST.ConditionalExpression: + return this['ternary?:']( + this.recurse(ast.test), + this.recurse(ast.alternate), + this.recurse(ast.consequent), + context + ); + case AST.Identifier: + return self.identifier(ast.name, context, create); + case AST.MemberExpression: + left = this.recurse(ast.object, false, !!create); + if (!ast.computed) { + right = ast.property.name; + } + if (ast.computed) right = this.recurse(ast.property); + return ast.computed ? + this.computedMember(left, right, context, create) : + this.nonComputedMember(left, right, context, create); + case AST.CallExpression: + args = []; + forEach(ast.arguments, function(expr) { + args.push(self.recurse(expr)); + }); + if (ast.filter) right = this.$filter(ast.callee.name); + if (!ast.filter) right = this.recurse(ast.callee, true); + return ast.filter ? + function(scope, locals, assign, inputs) { + var values = []; + for (var i = 0; i < args.length; ++i) { + values.push(args[i](scope, locals, assign, inputs)); + } + var value = right.apply(undefined, values, inputs); + return context ? {context: undefined, name: undefined, value: value} : value; + } : + function(scope, locals, assign, inputs) { + var rhs = right(scope, locals, assign, inputs); + var value; + if (rhs.value != null) { + var values = []; + for (var i = 0; i < args.length; ++i) { + values.push(args[i](scope, locals, assign, inputs)); + } + value = rhs.value.apply(rhs.context, values); + } + return context ? {value: value} : value; + }; + case AST.AssignmentExpression: + left = this.recurse(ast.left, true, 1); + right = this.recurse(ast.right); + return function(scope, locals, assign, inputs) { + var lhs = left(scope, locals, assign, inputs); + var rhs = right(scope, locals, assign, inputs); + lhs.context[lhs.name] = rhs; + return context ? {value: rhs} : rhs; + }; + case AST.ArrayExpression: + args = []; + forEach(ast.elements, function(expr) { + args.push(self.recurse(expr)); + }); + return function(scope, locals, assign, inputs) { + var value = []; + for (var i = 0; i < args.length; ++i) { + value.push(args[i](scope, locals, assign, inputs)); + } + return context ? {value: value} : value; + }; + case AST.ObjectExpression: + args = []; + forEach(ast.properties, function(property) { + if (property.computed) { + args.push({key: self.recurse(property.key), + computed: true, + value: self.recurse(property.value) + }); + } else { + args.push({key: property.key.type === AST.Identifier ? + property.key.name : + ('' + property.key.value), + computed: false, + value: self.recurse(property.value) + }); + } + }); + return function(scope, locals, assign, inputs) { + var value = {}; + for (var i = 0; i < args.length; ++i) { + if (args[i].computed) { + value[args[i].key(scope, locals, assign, inputs)] = args[i].value(scope, locals, assign, inputs); + } else { + value[args[i].key] = args[i].value(scope, locals, assign, inputs); + } + } + return context ? {value: value} : value; + }; + case AST.ThisExpression: + return function(scope) { + return context ? {value: scope} : scope; + }; + case AST.LocalsExpression: + return function(scope, locals) { + return context ? {value: locals} : locals; + }; + case AST.NGValueParameter: + return function(scope, locals, assign) { + return context ? {value: assign} : assign; + }; + } + }, - // Only cache the value if it's not going to mess up the cache object - // This is more performant that using Object.prototype.hasOwnProperty.call - if (path !== 'hasOwnProperty') { - getterFnCache[path] = fn; + 'unary+': function(argument, context) { + return function(scope, locals, assign, inputs) { + var arg = argument(scope, locals, assign, inputs); + if (isDefined(arg)) { + arg = +arg; + } else { + arg = 0; + } + return context ? {value: arg} : arg; + }; + }, + 'unary-': function(argument, context) { + return function(scope, locals, assign, inputs) { + var arg = argument(scope, locals, assign, inputs); + if (isDefined(arg)) { + arg = -arg; + } else { + arg = -0; + } + return context ? {value: arg} : arg; + }; + }, + 'unary!': function(argument, context) { + return function(scope, locals, assign, inputs) { + var arg = !argument(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + 'binary+': function(left, right, context) { + return function(scope, locals, assign, inputs) { + var lhs = left(scope, locals, assign, inputs); + var rhs = right(scope, locals, assign, inputs); + var arg = plusFn(lhs, rhs); + return context ? {value: arg} : arg; + }; + }, + 'binary-': function(left, right, context) { + return function(scope, locals, assign, inputs) { + var lhs = left(scope, locals, assign, inputs); + var rhs = right(scope, locals, assign, inputs); + var arg = (isDefined(lhs) ? lhs : 0) - (isDefined(rhs) ? rhs : 0); + return context ? {value: arg} : arg; + }; + }, + 'binary*': function(left, right, context) { + return function(scope, locals, assign, inputs) { + var arg = left(scope, locals, assign, inputs) * right(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + 'binary/': function(left, right, context) { + return function(scope, locals, assign, inputs) { + var arg = left(scope, locals, assign, inputs) / right(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + 'binary%': function(left, right, context) { + return function(scope, locals, assign, inputs) { + var arg = left(scope, locals, assign, inputs) % right(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + 'binary===': function(left, right, context) { + return function(scope, locals, assign, inputs) { + var arg = left(scope, locals, assign, inputs) === right(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + 'binary!==': function(left, right, context) { + return function(scope, locals, assign, inputs) { + var arg = left(scope, locals, assign, inputs) !== right(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + 'binary==': function(left, right, context) { + return function(scope, locals, assign, inputs) { + // eslint-disable-next-line eqeqeq + var arg = left(scope, locals, assign, inputs) == right(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + 'binary!=': function(left, right, context) { + return function(scope, locals, assign, inputs) { + // eslint-disable-next-line eqeqeq + var arg = left(scope, locals, assign, inputs) != right(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + 'binary<': function(left, right, context) { + return function(scope, locals, assign, inputs) { + var arg = left(scope, locals, assign, inputs) < right(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + 'binary>': function(left, right, context) { + return function(scope, locals, assign, inputs) { + var arg = left(scope, locals, assign, inputs) > right(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + 'binary<=': function(left, right, context) { + return function(scope, locals, assign, inputs) { + var arg = left(scope, locals, assign, inputs) <= right(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + 'binary>=': function(left, right, context) { + return function(scope, locals, assign, inputs) { + var arg = left(scope, locals, assign, inputs) >= right(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + 'binary&&': function(left, right, context) { + return function(scope, locals, assign, inputs) { + var arg = left(scope, locals, assign, inputs) && right(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + 'binary||': function(left, right, context) { + return function(scope, locals, assign, inputs) { + var arg = left(scope, locals, assign, inputs) || right(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + 'ternary?:': function(test, alternate, consequent, context) { + return function(scope, locals, assign, inputs) { + var arg = test(scope, locals, assign, inputs) ? alternate(scope, locals, assign, inputs) : consequent(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + value: function(value, context) { + return function() { return context ? {context: undefined, name: undefined, value: value} : value; }; + }, + identifier: function(name, context, create) { + return function(scope, locals, assign, inputs) { + var base = locals && (name in locals) ? locals : scope; + if (create && create !== 1 && base && base[name] == null) { + base[name] = {}; + } + var value = base ? base[name] : undefined; + if (context) { + return {context: base, name: name, value: value}; + } else { + return value; + } + }; + }, + computedMember: function(left, right, context, create) { + return function(scope, locals, assign, inputs) { + var lhs = left(scope, locals, assign, inputs); + var rhs; + var value; + if (lhs != null) { + rhs = right(scope, locals, assign, inputs); + rhs = getStringValue(rhs); + if (create && create !== 1) { + if (lhs && !(lhs[rhs])) { + lhs[rhs] = {}; + } + } + value = lhs[rhs]; + } + if (context) { + return {context: lhs, name: rhs, value: value}; + } else { + return value; + } + }; + }, + nonComputedMember: function(left, right, context, create) { + return function(scope, locals, assign, inputs) { + var lhs = left(scope, locals, assign, inputs); + if (create && create !== 1) { + if (lhs && lhs[right] == null) { + lhs[right] = {}; + } + } + var value = lhs != null ? lhs[right] : undefined; + if (context) { + return {context: lhs, name: right, value: value}; + } else { + return value; + } + }; + }, + inputs: function(input, watchId) { + return function(scope, value, locals, inputs) { + if (inputs) return inputs[watchId]; + return input(scope, value, locals); + }; } - return fn; + }; + + /** + * @constructor + */ + function Parser(lexer, $filter, options) { + this.ast = new AST(lexer, options); + this.astCompiler = options.csp ? new ASTInterpreter($filter) : + new ASTCompiler($filter); + } + + Parser.prototype = { + constructor: Parser, + + parse: function(text) { + var ast = this.ast.ast(text); + var fn = this.astCompiler.compile(ast); + fn.literal = isLiteral(ast); + fn.constant = isConstant(ast); + return fn; + } + }; + + function getValueOf(value) { + return isFunction(value.valueOf) ? value.valueOf() : objectValueOf.call(value); } /////////////////////////////////// /** - * @ngdoc function - * @name ng.$parse - * @function + * @ngdoc service + * @name $parse + * @kind function * * @description * * Converts Angular {@link guide/expression expression} into a function. * - *
    +   * ```js
        *   var getter = $parse('user.name');
        *   var setter = getter.assign;
        *   var context = {user:{name:'angular'}};
    @@ -10524,7 +16380,7 @@
        *   setter(context, 'newValue');
        *   expect(context.user.name).toEqual('newValue');
        *   expect(getter(context, locals)).toEqual('local');
    -   * 
    + * ``` * * * @param {string} expression String expression to compile. @@ -10547,156 +16403,351 @@ /** - * @ngdoc object - * @name ng.$parseProvider - * @function + * @ngdoc provider + * @name $parseProvider + * @this * * @description * `$parseProvider` can be used for configuring the default behavior of the {@link ng.$parse $parse} * service. */ function $ParseProvider() { - var cache = {}; - - var $parseOptions = { - csp: false, - unwrapPromises: false, - logPromiseWarnings: true + var cache = createMap(); + var literals = { + 'true': true, + 'false': false, + 'null': null, + 'undefined': undefined }; - + var identStart, identContinue; /** - * @deprecated Promise unwrapping via $parse is deprecated and will be removed in the future. - * * @ngdoc method - * @name ng.$parseProvider#unwrapPromises - * @methodOf ng.$parseProvider + * @name $parseProvider#addLiteral * @description * - * **This feature is deprecated, see deprecation notes below for more info** + * Configure $parse service to add literal values that will be present as literal at expressions. * - * If set to true (default is false), $parse will unwrap promises automatically when a promise is - * found at any part of the expression. In other words, if set to true, the expression will always - * result in a non-promise value. + * @param {string} literalName Token for the literal value. The literal name value must be a valid literal name. + * @param {*} literalValue Value for this literal. All literal values must be primitives or `undefined`. * - * While the promise is unresolved, it's treated as undefined, but once resolved and fulfilled, - * the fulfillment value is used in place of the promise while evaluating the expression. - * - * **Deprecation notice** - * - * This is a feature that didn't prove to be wildly useful or popular, primarily because of the - * dichotomy between data access in templates (accessed as raw values) and controller code - * (accessed as promises). - * - * In most code we ended up resolving promises manually in controllers anyway and thus unifying - * the model access there. - * - * Other downsides of automatic promise unwrapping: - * - * - when building components it's often desirable to receive the raw promises - * - adds complexity and slows down expression evaluation - * - makes expression code pre-generation unattractive due to the amount of code that needs to be - * generated - * - makes IDE auto-completion and tool support hard - * - * **Warning Logs** - * - * If the unwrapping is enabled, Angular will log a warning about each expression that unwraps a - * promise (to reduce the noise, each expression is logged only once). To disable this logging use - * `$parseProvider.logPromiseWarnings(false)` api. - * - * - * @param {boolean=} value New value. - * @returns {boolean|self} Returns the current setting when used as getter and self if used as - * setter. - */ - this.unwrapPromises = function(value) { - if (isDefined(value)) { - $parseOptions.unwrapPromises = !!value; - return this; - } else { - return $parseOptions.unwrapPromises; - } + **/ + this.addLiteral = function(literalName, literalValue) { + literals[literalName] = literalValue; }; - /** - * @deprecated Promise unwrapping via $parse is deprecated and will be removed in the future. - * * @ngdoc method - * @name ng.$parseProvider#logPromiseWarnings - * @methodOf ng.$parseProvider + * @name $parseProvider#setIdentifierFns + * * @description * - * Controls whether Angular should log a warning on any encounter of a promise in an expression. + * Allows defining the set of characters that are allowed in Angular expressions. The function + * `identifierStart` will get called to know if a given character is a valid character to be the + * first character for an identifier. The function `identifierContinue` will get called to know if + * a given character is a valid character to be a follow-up identifier character. The functions + * `identifierStart` and `identifierContinue` will receive as arguments the single character to be + * identifier and the character code point. These arguments will be `string` and `numeric`. Keep in + * mind that the `string` parameter can be two characters long depending on the character + * representation. It is expected for the function to return `true` or `false`, whether that + * character is allowed or not. * - * The default is set to `true`. + * Since this function will be called extensively, keep the implementation of these functions fast, + * as the performance of these functions have a direct impact on the expressions parsing speed. * - * This setting applies only if `$parseProvider.unwrapPromises` setting is set to true as well. - * - * @param {boolean=} value New value. - * @returns {boolean|self} Returns the current setting when used as getter and self if used as - * setter. + * @param {function=} identifierStart The function that will decide whether the given character is + * a valid identifier start character. + * @param {function=} identifierContinue The function that will decide whether the given character is + * a valid identifier continue character. */ - this.logPromiseWarnings = function(value) { - if (isDefined(value)) { - $parseOptions.logPromiseWarnings = value; - return this; - } else { - return $parseOptions.logPromiseWarnings; - } + this.setIdentifierFns = function(identifierStart, identifierContinue) { + identStart = identifierStart; + identContinue = identifierContinue; + return this; }; - - this.$get = ['$filter', '$sniffer', '$log', function($filter, $sniffer, $log) { - $parseOptions.csp = $sniffer.csp; - - promiseWarning = function promiseWarningFn(fullExp) { - if (!$parseOptions.logPromiseWarnings || promiseWarningCache.hasOwnProperty(fullExp)) return; - promiseWarningCache[fullExp] = true; - $log.warn('[$parse] Promise found in the expression `' + fullExp + '`. ' + - 'Automatic unwrapping of promises in Angular expressions is deprecated.'); + this.$get = ['$filter', function($filter) { + var noUnsafeEval = csp().noUnsafeEval; + var $parseOptions = { + csp: noUnsafeEval, + literals: copy(literals), + isIdentifierStart: isFunction(identStart) && identStart, + isIdentifierContinue: isFunction(identContinue) && identContinue }; + return $parse; - return function(exp) { - var parsedExpression; + function $parse(exp, interceptorFn) { + var parsedExpression, oneTime, cacheKey; switch (typeof exp) { case 'string': + exp = exp.trim(); + cacheKey = exp; - if (cache.hasOwnProperty(exp)) { - return cache[exp]; + parsedExpression = cache[cacheKey]; + + if (!parsedExpression) { + if (exp.charAt(0) === ':' && exp.charAt(1) === ':') { + oneTime = true; + exp = exp.substring(2); + } + var lexer = new Lexer($parseOptions); + var parser = new Parser(lexer, $filter, $parseOptions); + parsedExpression = parser.parse(exp); + if (parsedExpression.constant) { + parsedExpression.$$watchDelegate = constantWatchDelegate; + } else if (oneTime) { + parsedExpression.$$watchDelegate = parsedExpression.literal ? + oneTimeLiteralWatchDelegate : oneTimeWatchDelegate; + } else if (parsedExpression.inputs) { + parsedExpression.$$watchDelegate = inputsWatchDelegate; + } + cache[cacheKey] = parsedExpression; } - - var lexer = new Lexer($parseOptions); - var parser = new Parser(lexer, $filter, $parseOptions); - parsedExpression = parser.parse(exp, false); - - if (exp !== 'hasOwnProperty') { - // Only cache the value if it's not going to mess up the cache object - // This is more performant that using Object.prototype.hasOwnProperty.call - cache[exp] = parsedExpression; - } - - return parsedExpression; + return addInterceptor(parsedExpression, interceptorFn); case 'function': - return exp; + return addInterceptor(exp, interceptorFn); default: - return noop; + return addInterceptor(noop, interceptorFn); } - }; + } + + function expressionInputDirtyCheck(newValue, oldValueOfValue, compareObjectIdentity) { + + if (newValue == null || oldValueOfValue == null) { // null/undefined + return newValue === oldValueOfValue; + } + + if (typeof newValue === 'object') { + + // attempt to convert the value to a primitive type + // TODO(docs): add a note to docs that by implementing valueOf even objects and arrays can + // be cheaply dirty-checked + newValue = getValueOf(newValue); + + if (typeof newValue === 'object' && !compareObjectIdentity) { + // objects/arrays are not supported - deep-watching them would be too expensive + return false; + } + + // fall-through to the primitive equality check + } + + //Primitive or NaN + // eslint-disable-next-line no-self-compare + return newValue === oldValueOfValue || (newValue !== newValue && oldValueOfValue !== oldValueOfValue); + } + + function inputsWatchDelegate(scope, listener, objectEquality, parsedExpression, prettyPrintExpression) { + var inputExpressions = parsedExpression.inputs; + var lastResult; + + if (inputExpressions.length === 1) { + var oldInputValueOf = expressionInputDirtyCheck; // init to something unique so that equals check fails + inputExpressions = inputExpressions[0]; + return scope.$watch(function expressionInputWatch(scope) { + var newInputValue = inputExpressions(scope); + if (!expressionInputDirtyCheck(newInputValue, oldInputValueOf, inputExpressions.isPure)) { + lastResult = parsedExpression(scope, undefined, undefined, [newInputValue]); + oldInputValueOf = newInputValue && getValueOf(newInputValue); + } + return lastResult; + }, listener, objectEquality, prettyPrintExpression); + } + + var oldInputValueOfValues = []; + var oldInputValues = []; + for (var i = 0, ii = inputExpressions.length; i < ii; i++) { + oldInputValueOfValues[i] = expressionInputDirtyCheck; // init to something unique so that equals check fails + oldInputValues[i] = null; + } + + return scope.$watch(function expressionInputsWatch(scope) { + var changed = false; + + for (var i = 0, ii = inputExpressions.length; i < ii; i++) { + var newInputValue = inputExpressions[i](scope); + if (changed || (changed = !expressionInputDirtyCheck(newInputValue, oldInputValueOfValues[i], inputExpressions[i].isPure))) { + oldInputValues[i] = newInputValue; + oldInputValueOfValues[i] = newInputValue && getValueOf(newInputValue); + } + } + + if (changed) { + lastResult = parsedExpression(scope, undefined, undefined, oldInputValues); + } + + return lastResult; + }, listener, objectEquality, prettyPrintExpression); + } + + function oneTimeWatchDelegate(scope, listener, objectEquality, parsedExpression, prettyPrintExpression) { + var unwatch, lastValue; + if (parsedExpression.inputs) { + unwatch = inputsWatchDelegate(scope, oneTimeListener, objectEquality, parsedExpression, prettyPrintExpression); + } else { + unwatch = scope.$watch(oneTimeWatch, oneTimeListener, objectEquality); + } + return unwatch; + + function oneTimeWatch(scope) { + return parsedExpression(scope); + } + function oneTimeListener(value, old, scope) { + lastValue = value; + if (isFunction(listener)) { + listener(value, old, scope); + } + if (isDefined(value)) { + scope.$$postDigest(function() { + if (isDefined(lastValue)) { + unwatch(); + } + }); + } + } + } + + function oneTimeLiteralWatchDelegate(scope, listener, objectEquality, parsedExpression) { + var unwatch, lastValue; + unwatch = scope.$watch(function oneTimeWatch(scope) { + return parsedExpression(scope); + }, function oneTimeListener(value, old, scope) { + lastValue = value; + if (isFunction(listener)) { + listener(value, old, scope); + } + if (isAllDefined(value)) { + scope.$$postDigest(function() { + if (isAllDefined(lastValue)) unwatch(); + }); + } + }, objectEquality); + + return unwatch; + + function isAllDefined(value) { + var allDefined = true; + forEach(value, function(val) { + if (!isDefined(val)) allDefined = false; + }); + return allDefined; + } + } + + function constantWatchDelegate(scope, listener, objectEquality, parsedExpression) { + var unwatch = scope.$watch(function constantWatch(scope) { + unwatch(); + return parsedExpression(scope); + }, listener, objectEquality); + return unwatch; + } + + function addInterceptor(parsedExpression, interceptorFn) { + if (!interceptorFn) return parsedExpression; + var watchDelegate = parsedExpression.$$watchDelegate; + var useInputs = false; + + var regularWatch = + watchDelegate !== oneTimeLiteralWatchDelegate && + watchDelegate !== oneTimeWatchDelegate; + + var fn = regularWatch ? function regularInterceptedExpression(scope, locals, assign, inputs) { + var value = useInputs && inputs ? inputs[0] : parsedExpression(scope, locals, assign, inputs); + return interceptorFn(value, scope, locals); + } : function oneTimeInterceptedExpression(scope, locals, assign, inputs) { + var value = parsedExpression(scope, locals, assign, inputs); + var result = interceptorFn(value, scope, locals); + // we only return the interceptor's result if the + // initial value is defined (for bind-once) + return isDefined(value) ? result : value; + }; + + // Propagate $$watchDelegates other then inputsWatchDelegate + useInputs = !parsedExpression.inputs; + if (watchDelegate && watchDelegate !== inputsWatchDelegate) { + fn.$$watchDelegate = watchDelegate; + fn.inputs = parsedExpression.inputs; + } else if (!interceptorFn.$stateful) { + // Treat interceptor like filters - assume non-stateful by default and use the inputsWatchDelegate + fn.$$watchDelegate = inputsWatchDelegate; + fn.inputs = parsedExpression.inputs ? parsedExpression.inputs : [parsedExpression]; + } + + if (fn.inputs) { + fn.inputs = fn.inputs.map(function(e) { + // Remove the isPure flag of inputs when it is not absolute because they are now wrapped in a + // potentially non-pure interceptor function. + if (e.isPure === PURITY_RELATIVE) { + return function depurifier(s) { return e(s); }; + } + return e; + }); + } + + return fn; + } }]; } /** * @ngdoc service - * @name ng.$q + * @name $q * @requires $rootScope * * @description - * A promise/deferred implementation inspired by [Kris Kowal's Q](https://github.com/kriskowal/q). + * A service that helps you run functions asynchronously, and use their return values (or exceptions) + * when they are done processing. + * + * This is a [Promises/A+](https://promisesaplus.com/)-compliant implementation of promises/deferred + * objects inspired by [Kris Kowal's Q](https://github.com/kriskowal/q). + * + * $q can be used in two fashions --- one which is more similar to Kris Kowal's Q or jQuery's Deferred + * implementations, and the other which resembles ES6 (ES2015) promises to some degree. + * + * # $q constructor + * + * The streamlined ES6 style promise is essentially just using $q as a constructor which takes a `resolver` + * function as the first argument. This is similar to the native Promise implementation from ES6, + * see [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise). + * + * While the constructor-style use is supported, not all of the supporting methods from ES6 promises are + * available yet. + * + * It can be used like so: + * + * ```js + * // for the purpose of this example let's assume that variables `$q` and `okToGreet` + * // are available in the current lexical scope (they could have been injected or passed in). + * + * function asyncGreet(name) { + * // perform some asynchronous operation, resolve or reject the promise when appropriate. + * return $q(function(resolve, reject) { + * setTimeout(function() { + * if (okToGreet(name)) { + * resolve('Hello, ' + name + '!'); + * } else { + * reject('Greeting ' + name + ' is not allowed.'); + * } + * }, 1000); + * }); + * } + * + * var promise = asyncGreet('Robin Hood'); + * promise.then(function(greeting) { + * alert('Success: ' + greeting); + * }, function(reason) { + * alert('Failed: ' + reason); + * }); + * ``` + * + * Note: progress/notify callbacks are not currently supported via the ES6-style interface. + * + * Note: unlike ES6 behavior, an exception thrown in the constructor function will NOT implicitly reject the promise. + * + * However, the more traditional CommonJS-style usage is still available, and documented below. * * [The CommonJS Promise proposal](http://wiki.commonjs.org/wiki/Promises) describes a promise as an * interface for interacting with an object that represents the result of an action that is @@ -10705,25 +16756,21 @@ * From the perspective of dealing with error handling, deferred and promise APIs are to * asynchronous programming what `try`, `catch` and `throw` keywords are to synchronous programming. * - *
    -   *   // for the purpose of this example let's assume that variables `$q` and `scope` are
    -   *   // available in the current lexical scope (they could have been injected or passed in).
    +   * ```js
    +   *   // for the purpose of this example let's assume that variables `$q` and `okToGreet`
    +   *   // are available in the current lexical scope (they could have been injected or passed in).
        *
        *   function asyncGreet(name) {
      *     var deferred = $q.defer();
      *
      *     setTimeout(function() {
    - *       // since this fn executes async in a future turn of the event loop, we need to wrap
    - *       // our code into an $apply call so that the model changes are properly observed.
    - *       scope.$apply(function() {
    - *         deferred.notify('About to greet ' + name + '.');
    + *       deferred.notify('About to greet ' + name + '.');
      *
    - *         if (okToGreet(name)) {
    - *           deferred.resolve('Hello, ' + name + '!');
    - *         } else {
    - *           deferred.reject('Greeting ' + name + ' is not allowed.');
    - *         }
    - *       });
    + *       if (okToGreet(name)) {
    + *         deferred.resolve('Hello, ' + name + '!');
    + *       } else {
    + *         deferred.reject('Greeting ' + name + ' is not allowed.');
    + *       }
      *     }, 1000);
      *
      *     return deferred.promise;
    @@ -10737,7 +16784,7 @@
      *   }, function(update) {
      *     alert('Got notification: ' + update);
      *   });
    -   * 
    + * ``` * * At first it might not be obvious why this extra complexity is worth the trouble. The payoff * comes in the way of guarantees that promise and deferred APIs make, see @@ -10748,7 +16795,6 @@ * For more on this please see the [Q documentation](https://github.com/kriskowal/q) especially the * section on serial or parallel joining of promises. * - * * # The Deferred API * * A new instance of deferred is constructed by calling `$q.defer()`. @@ -10763,7 +16809,7 @@ * constructed via `$q.reject`, the promise will be rejected instead. * - `reject(reason)` – rejects the derived promise with the `reason`. This is equivalent to * resolving it with a rejection constructed via `$q.reject`. - * - `notify(value)` - provides updates on the status of the promises execution. This may be called + * - `notify(value)` - provides updates on the status of the promise's execution. This may be called * multiple times before the promise is either resolved or rejected. * * **Properties** @@ -10781,42 +16827,41 @@ * * **Methods** * - * - `then(successCallback, errorCallback, notifyCallback)` – regardless of when the promise was or + * - `then(successCallback, [errorCallback], [notifyCallback])` – regardless of when the promise was or * will be resolved or rejected, `then` calls one of the success or error callbacks asynchronously * as soon as the result is available. The callbacks are called with a single argument: the result * or rejection reason. Additionally, the notify callback may be called zero or more times to * provide a progress indication, before the promise is resolved or rejected. * * This method *returns a new promise* which is resolved or rejected via the return value of the - * `successCallback`, `errorCallback`. It also notifies via the return value of the - * `notifyCallback` method. The promise can not be resolved or rejected from the notifyCallback - * method. + * `successCallback`, `errorCallback` (unless that value is a promise, in which case it is resolved + * with the value which is resolved in that promise using + * [promise chaining](http://www.html5rocks.com/en/tutorials/es6/promises/#toc-promises-queues)). + * It also notifies via the return value of the `notifyCallback` method. The promise cannot be + * resolved or rejected from the notifyCallback method. The errorCallback and notifyCallback + * arguments are optional. * * - `catch(errorCallback)` – shorthand for `promise.then(null, errorCallback)` * - * - `finally(callback)` – allows you to observe either the fulfillment or rejection of a promise, + * - `finally(callback, notifyCallback)` – allows you to observe either the fulfillment or rejection of a promise, * but to do so without modifying the final value. This is useful to release resources or do some * clean-up that needs to be done whether the promise was rejected or resolved. See the [full * specification](https://github.com/kriskowal/q/wiki/API-Reference#promisefinallycallback) for * more information. * - * Because `finally` is a reserved word in JavaScript and reserved keywords are not supported as - * property names by ES3, you'll need to invoke the method like `promise['finally'](callback)` to - * make your code IE8 compatible. - * * # Chaining promises * * Because calling the `then` method of a promise returns a new derived promise, it is easily * possible to create a chain of promises: * - *
    +   * ```js
        *   promiseB = promiseA.then(function(result) {
      *     return result + 1;
      *   });
        *
        *   // promiseB will be resolved immediately after promiseA is resolved and its value
        *   // will be the result of promiseA incremented by 1
    -   * 
    + * ``` * * It is possible to create chains of any length and since a promise can be resolved with another * promise (which will defer its resolution further), it is possible to pause/defer resolution of @@ -10834,9 +16879,9 @@ * - Q has many more features than $q, but that comes at a cost of bytes. $q is tiny, but contains * all the important functionality needed for common async tasks. * - * # Testing + * # Testing * - *
    +   *  ```js
        *    it('should simulate promise', inject(function($q, $rootScope) {
      *      var deferred = $q.defer();
      *      var promise = deferred.promise;
    @@ -10856,17 +16901,70 @@
      *      $rootScope.$apply();
      *      expect(resolvedValue).toEqual(123);
      *    }));
    -   *  
    + * ``` + * + * @param {function(function, function)} resolver Function which is responsible for resolving or + * rejecting the newly created promise. The first parameter is a function which resolves the + * promise, the second parameter is a function which rejects the promise. + * + * @returns {Promise} The newly created promise. + */ + /** + * @ngdoc provider + * @name $qProvider + * @this + * + * @description */ function $QProvider() { - + var errorOnUnhandledRejections = true; this.$get = ['$rootScope', '$exceptionHandler', function($rootScope, $exceptionHandler) { return qFactory(function(callback) { $rootScope.$evalAsync(callback); - }, $exceptionHandler); + }, $exceptionHandler, errorOnUnhandledRejections); }]; + + /** + * @ngdoc method + * @name $qProvider#errorOnUnhandledRejections + * @kind function + * + * @description + * Retrieves or overrides whether to generate an error when a rejected promise is not handled. + * This feature is enabled by default. + * + * @param {boolean=} value Whether to generate an error when a rejected promise is not handled. + * @returns {boolean|ng.$qProvider} Current value when called without a new value or self for + * chaining otherwise. + */ + this.errorOnUnhandledRejections = function(value) { + if (isDefined(value)) { + errorOnUnhandledRejections = value; + return this; + } else { + return errorOnUnhandledRejections; + } + }; } + /** @this */ + function $$QProvider() { + var errorOnUnhandledRejections = true; + this.$get = ['$browser', '$exceptionHandler', function($browser, $exceptionHandler) { + return qFactory(function(callback) { + $browser.defer(callback); + }, $exceptionHandler, errorOnUnhandledRejections); + }]; + + this.errorOnUnhandledRejections = function(value) { + if (isDefined(value)) { + errorOnUnhandledRejections = value; + return this; + } else { + return errorOnUnhandledRejections; + } + }; + } /** * Constructs a promise manager. @@ -10874,170 +16972,209 @@ * @param {function(function)} nextTick Function for executing functions in the next turn. * @param {function(...*)} exceptionHandler Function into which unexpected exceptions are passed for * debugging purposes. + * @param {boolean=} errorOnUnhandledRejections Whether an error should be generated on unhandled + * promises rejections. * @returns {object} Promise manager. */ - function qFactory(nextTick, exceptionHandler) { + function qFactory(nextTick, exceptionHandler, errorOnUnhandledRejections) { + var $qMinErr = minErr('$q', TypeError); + var queueSize = 0; + var checkQueue = []; /** - * @ngdoc + * @ngdoc method * @name ng.$q#defer - * @methodOf ng.$q + * @kind function + * * @description * Creates a `Deferred` object which represents a task which will finish in the future. * * @returns {Deferred} Returns a new instance of deferred. */ - var defer = function() { - var pending = [], - value, deferred; + function defer() { + return new Deferred(); + } - deferred = { - - resolve: function(val) { - if (pending) { - var callbacks = pending; - pending = undefined; - value = ref(val); - - if (callbacks.length) { - nextTick(function() { - var callback; - for (var i = 0, ii = callbacks.length; i < ii; i++) { - callback = callbacks[i]; - value.then(callback[0], callback[1], callback[2]); - } - }); - } - } - }, + function Deferred() { + var promise = this.promise = new Promise(); + //Non prototype methods necessary to support unbound execution :/ + this.resolve = function(val) { resolvePromise(promise, val); }; + this.reject = function(reason) { rejectPromise(promise, reason); }; + this.notify = function(progress) { notifyPromise(promise, progress); }; + } - reject: function(reason) { - deferred.resolve(reject(reason)); - }, + function Promise() { + this.$$state = { status: 0 }; + } + extend(Promise.prototype, { + then: function(onFulfilled, onRejected, progressBack) { + if (isUndefined(onFulfilled) && isUndefined(onRejected) && isUndefined(progressBack)) { + return this; + } + var result = new Promise(); - notify: function(progress) { - if (pending) { - var callbacks = pending; + this.$$state.pending = this.$$state.pending || []; + this.$$state.pending.push([result, onFulfilled, onRejected, progressBack]); + if (this.$$state.status > 0) scheduleProcessQueue(this.$$state); - if (pending.length) { - nextTick(function() { - var callback; - for (var i = 0, ii = callbacks.length; i < ii; i++) { - callback = callbacks[i]; - callback[2](progress); - } - }); - } - } - }, + return result; + }, + 'catch': function(callback) { + return this.then(null, callback); + }, - promise: { - then: function(callback, errback, progressback) { - var result = defer(); + 'finally': function(callback, progressBack) { + return this.then(function(value) { + return handleCallback(value, resolve, callback); + }, function(error) { + return handleCallback(error, reject, callback); + }, progressBack); + } + }); - var wrappedCallback = function(value) { - try { - result.resolve((isFunction(callback) ? callback : defaultCallback)(value)); - } catch(e) { - result.reject(e); - exceptionHandler(e); - } - }; + function processQueue(state) { + var fn, promise, pending; - var wrappedErrback = function(reason) { - try { - result.resolve((isFunction(errback) ? errback : defaultErrback)(reason)); - } catch(e) { - result.reject(e); - exceptionHandler(e); - } - }; - - var wrappedProgressback = function(progress) { - try { - result.notify((isFunction(progressback) ? progressback : defaultCallback)(progress)); - } catch(e) { - exceptionHandler(e); - } - }; - - if (pending) { - pending.push([wrappedCallback, wrappedErrback, wrappedProgressback]); + pending = state.pending; + state.processScheduled = false; + state.pending = undefined; + try { + for (var i = 0, ii = pending.length; i < ii; ++i) { + markQStateExceptionHandled(state); + promise = pending[i][0]; + fn = pending[i][state.status]; + try { + if (isFunction(fn)) { + resolvePromise(promise, fn(state.value)); + } else if (state.status === 1) { + resolvePromise(promise, state.value); } else { - value.then(wrappedCallback, wrappedErrback, wrappedProgressback); + rejectPromise(promise, state.value); } - - return result.promise; - }, - - "catch": function(callback) { - return this.then(null, callback); - }, - - "finally": function(callback) { - - function makePromise(value, resolved) { - var result = defer(); - if (resolved) { - result.resolve(value); - } else { - result.reject(value); - } - return result.promise; - } - - function handleCallback(value, isResolved) { - var callbackOutput = null; - try { - callbackOutput = (callback ||defaultCallback)(); - } catch(e) { - return makePromise(e, false); - } - if (callbackOutput && isFunction(callbackOutput.then)) { - return callbackOutput.then(function() { - return makePromise(value, isResolved); - }, function(error) { - return makePromise(error, false); - }); - } else { - return makePromise(value, isResolved); - } - } - - return this.then(function(value) { - return handleCallback(value, true); - }, function(error) { - return handleCallback(error, false); - }); + } catch (e) { + rejectPromise(promise, e); } } - }; - - return deferred; - }; - - - var ref = function(value) { - if (value && isFunction(value.then)) return value; - return { - then: function(callback) { - var result = defer(); - nextTick(function() { - result.resolve(callback(value)); - }); - return result.promise; + } finally { + --queueSize; + if (errorOnUnhandledRejections && queueSize === 0) { + nextTick(processChecks); } - }; - }; + } + } + function processChecks() { + // eslint-disable-next-line no-unmodified-loop-condition + while (!queueSize && checkQueue.length) { + var toCheck = checkQueue.shift(); + if (!isStateExceptionHandled(toCheck)) { + markQStateExceptionHandled(toCheck); + var errorMessage = 'Possibly unhandled rejection: ' + toDebugString(toCheck.value); + if (isError(toCheck.value)) { + exceptionHandler(toCheck.value, errorMessage); + } else { + exceptionHandler(errorMessage); + } + } + } + } + + function scheduleProcessQueue(state) { + if (errorOnUnhandledRejections && !state.pending && state.status === 2 && !isStateExceptionHandled(state)) { + if (queueSize === 0 && checkQueue.length === 0) { + nextTick(processChecks); + } + checkQueue.push(state); + } + if (state.processScheduled || !state.pending) return; + state.processScheduled = true; + ++queueSize; + nextTick(function() { processQueue(state); }); + } + + function resolvePromise(promise, val) { + if (promise.$$state.status) return; + if (val === promise) { + $$reject(promise, $qMinErr( + 'qcycle', + 'Expected promise to be resolved with value other than itself \'{0}\'', + val)); + } else { + $$resolve(promise, val); + } + + } + + function $$resolve(promise, val) { + var then; + var done = false; + try { + if (isObject(val) || isFunction(val)) then = val.then; + if (isFunction(then)) { + promise.$$state.status = -1; + then.call(val, doResolve, doReject, doNotify); + } else { + promise.$$state.value = val; + promise.$$state.status = 1; + scheduleProcessQueue(promise.$$state); + } + } catch (e) { + doReject(e); + } + + function doResolve(val) { + if (done) return; + done = true; + $$resolve(promise, val); + } + function doReject(val) { + if (done) return; + done = true; + $$reject(promise, val); + } + function doNotify(progress) { + notifyPromise(promise, progress); + } + } + + function rejectPromise(promise, reason) { + if (promise.$$state.status) return; + $$reject(promise, reason); + } + + function $$reject(promise, reason) { + promise.$$state.value = reason; + promise.$$state.status = 2; + scheduleProcessQueue(promise.$$state); + } + + function notifyPromise(promise, progress) { + var callbacks = promise.$$state.pending; + + if ((promise.$$state.status <= 0) && callbacks && callbacks.length) { + nextTick(function() { + var callback, result; + for (var i = 0, ii = callbacks.length; i < ii; i++) { + result = callbacks[i][0]; + callback = callbacks[i][3]; + try { + notifyPromise(result, isFunction(callback) ? callback(progress) : progress); + } catch (e) { + exceptionHandler(e); + } + } + }); + } + } /** - * @ngdoc - * @name ng.$q#reject - * @methodOf ng.$q + * @ngdoc method + * @name $q#reject + * @kind function + * * @description * Creates a promise that is resolved as rejected with the specified `reason`. This api should be * used to forward rejection in a chain of promises. If you are dealing with the last promise in @@ -11049,7 +17186,7 @@ * current promise, you have to "rethrow" the error by returning a rejection constructed via * `reject`. * - *
    +     * ```js
          *   promiseB = promiseA.then(function(result) {
        *     // success: do something and resolve promiseB
        *     //          with the old or a new result
    @@ -11064,104 +17201,78 @@
        *     }
        *     return $q.reject(reason);
        *   });
    -     * 
    + * ``` * * @param {*} reason Constant, message, exception or an object representing the rejection reason. * @returns {Promise} Returns a promise that was already resolved as rejected with the `reason`. */ - var reject = function(reason) { - return { - then: function(callback, errback) { - var result = defer(); - nextTick(function() { - try { - result.resolve((isFunction(errback) ? errback : defaultErrback)(reason)); - } catch(e) { - result.reject(e); - exceptionHandler(e); - } - }); - return result.promise; - } - }; - }; + function reject(reason) { + var result = new Promise(); + rejectPromise(result, reason); + return result; + } + function handleCallback(value, resolver, callback) { + var callbackOutput = null; + try { + if (isFunction(callback)) callbackOutput = callback(); + } catch (e) { + return reject(e); + } + if (isPromiseLike(callbackOutput)) { + return callbackOutput.then(function() { + return resolver(value); + }, reject); + } else { + return resolver(value); + } + } /** - * @ngdoc - * @name ng.$q#when - * @methodOf ng.$q + * @ngdoc method + * @name $q#when + * @kind function + * * @description * Wraps an object that might be a value or a (3rd party) then-able promise into a $q promise. * This is useful when you are dealing with an object that might or might not be a promise, or if * the promise comes from a source that can't be trusted. * * @param {*} value Value or a promise + * @param {Function=} successCallback + * @param {Function=} errorCallback + * @param {Function=} progressCallback * @returns {Promise} Returns a promise of the passed value or promise */ - var when = function(value, callback, errback, progressback) { - var result = defer(), - done; - - var wrappedCallback = function(value) { - try { - return (isFunction(callback) ? callback : defaultCallback)(value); - } catch (e) { - exceptionHandler(e); - return reject(e); - } - }; - - var wrappedErrback = function(reason) { - try { - return (isFunction(errback) ? errback : defaultErrback)(reason); - } catch (e) { - exceptionHandler(e); - return reject(e); - } - }; - - var wrappedProgressback = function(progress) { - try { - return (isFunction(progressback) ? progressback : defaultCallback)(progress); - } catch (e) { - exceptionHandler(e); - } - }; - - nextTick(function() { - ref(value).then(function(value) { - if (done) return; - done = true; - result.resolve(ref(value).then(wrappedCallback, wrappedErrback, wrappedProgressback)); - }, function(reason) { - if (done) return; - done = true; - result.resolve(wrappedErrback(reason)); - }, function(progress) { - if (done) return; - result.notify(wrappedProgressback(progress)); - }); - }); - - return result.promise; - }; - function defaultCallback(value) { - return value; + function when(value, callback, errback, progressBack) { + var result = new Promise(); + resolvePromise(result, value); + return result.then(callback, errback, progressBack); } - - function defaultErrback(reason) { - return reject(reason); - } - - /** - * @ngdoc - * @name ng.$q#all - * @methodOf ng.$q + * @ngdoc method + * @name $q#resolve + * @kind function + * + * @description + * Alias of {@link ng.$q#when when} to maintain naming consistency with ES6. + * + * @param {*} value Value or a promise + * @param {Function=} successCallback + * @param {Function=} errorCallback + * @param {Function=} progressCallback + * @returns {Promise} Returns a promise of the passed value or promise + */ + var resolve = when; + + /** + * @ngdoc method + * @name $q#all + * @kind function + * * @description * Combines multiple promises into a single promise that is resolved when all of the input * promises are resolved. @@ -11172,36 +17283,126 @@ * If any of the promises is resolved with a rejection, this resulting promise will be rejected * with the same rejection value. */ + function all(promises) { - var deferred = defer(), - counter = 0, - results = isArray(promises) ? [] : {}; + var result = new Promise(), + counter = 0, + results = isArray(promises) ? [] : {}; forEach(promises, function(promise, key) { counter++; - ref(promise).then(function(value) { - if (results.hasOwnProperty(key)) return; + when(promise).then(function(value) { results[key] = value; - if (!(--counter)) deferred.resolve(results); + if (!(--counter)) resolvePromise(result, results); }, function(reason) { - if (results.hasOwnProperty(key)) return; - deferred.reject(reason); + rejectPromise(result, reason); }); }); if (counter === 0) { - deferred.resolve(results); + resolvePromise(result, results); } + return result; + } + + /** + * @ngdoc method + * @name $q#race + * @kind function + * + * @description + * Returns a promise that resolves or rejects as soon as one of those promises + * resolves or rejects, with the value or reason from that promise. + * + * @param {Array.|Object.} promises An array or hash of promises. + * @returns {Promise} a promise that resolves or rejects as soon as one of the `promises` + * resolves or rejects, with the value or reason from that promise. + */ + + function race(promises) { + var deferred = defer(); + + forEach(promises, function(promise) { + when(promise).then(deferred.resolve, deferred.reject); + }); + return deferred.promise; } - return { - defer: defer, - reject: reject, - when: when, - all: all - }; + function $Q(resolver) { + if (!isFunction(resolver)) { + throw $qMinErr('norslvr', 'Expected resolverFn, got \'{0}\'', resolver); + } + + var promise = new Promise(); + + function resolveFn(value) { + resolvePromise(promise, value); + } + + function rejectFn(reason) { + rejectPromise(promise, reason); + } + + resolver(resolveFn, rejectFn); + + return promise; + } + + // Let's make the instanceof operator work for promises, so that + // `new $q(fn) instanceof $q` would evaluate to true. + $Q.prototype = Promise.prototype; + + $Q.defer = defer; + $Q.reject = reject; + $Q.when = when; + $Q.resolve = resolve; + $Q.all = all; + $Q.race = race; + + return $Q; + } + + function isStateExceptionHandled(state) { + return !!state.pur; + } + function markQStateExceptionHandled(state) { + state.pur = true; + } + function markQExceptionHandled(q) { + markQStateExceptionHandled(q.$$state); + } + + /** @this */ + function $$RAFProvider() { //rAF + this.$get = ['$window', '$timeout', function($window, $timeout) { + var requestAnimationFrame = $window.requestAnimationFrame || + $window.webkitRequestAnimationFrame; + + var cancelAnimationFrame = $window.cancelAnimationFrame || + $window.webkitCancelAnimationFrame || + $window.webkitCancelRequestAnimationFrame; + + var rafSupported = !!requestAnimationFrame; + var raf = rafSupported + ? function(fn) { + var id = requestAnimationFrame(fn); + return function() { + cancelAnimationFrame(id); + }; + } + : function(fn) { + var timer = $timeout(fn, 16.66, false); // 1000 / 60 = 16.666 + return function() { + $timeout.cancel(timer); + }; + }; + + raf.supported = rafSupported; + + return raf; + }]; } /** @@ -11218,30 +17419,29 @@ * exposed as $$____ properties * * Loop operations are optimized by using while(count--) { ... } - * - this means that in order to keep the same order of execution as addition we have to add - * items to the array at the beginning (shift) instead of at the end (push) + * - This means that in order to keep the same order of execution as addition we have to add + * items to the array at the beginning (unshift) instead of at the end (push) * * Child scopes are created and removed often - * - Using an array would be slow since inserts in middle are expensive so we use linked list + * - Using an array would be slow since inserts in the middle are expensive; so we use linked lists * - * There are few watches then a lot of observers. This is why you don't want the observer to be - * implemented in the same way as watch. Watch requires return of initialization function which - * are expensive to construct. + * There are fewer watches than observers. This is why you don't want the observer to be implemented + * in the same way as watch. Watch requires return of the initialization function which is expensive + * to construct. */ /** - * @ngdoc object - * @name ng.$rootScopeProvider + * @ngdoc provider + * @name $rootScopeProvider * @description * * Provider for the $rootScope service. */ /** - * @ngdoc function - * @name ng.$rootScopeProvider#digestTtl - * @methodOf ng.$rootScopeProvider + * @ngdoc method + * @name $rootScopeProvider#digestTtl * @description * * Sets the number of `$digest` iterations the scope should attempt to execute before giving up and @@ -11262,20 +17462,23 @@ /** - * @ngdoc object - * @name ng.$rootScope + * @ngdoc service + * @name $rootScope + * @this + * * @description * * Every application has a single root {@link ng.$rootScope.Scope scope}. * All other scopes are descendant scopes of the root scope. Scopes provide separation * between the model and the view, via a mechanism for watching the model for changes. - * They also provide an event emission/broadcast and subscription facility. See the + * They also provide event emission/broadcast and subscription facility. See the * {@link guide/scope developer guide on scopes}. */ - function $RootScopeProvider(){ + function $RootScopeProvider() { var TTL = 10; var $rootScopeMinErr = minErr('$rootScope'); var lastDirtyWatch = null; + var applyAsyncId = null; this.digestTtl = function(value) { if (arguments.length) { @@ -11284,38 +17487,84 @@ return TTL; }; - this.$get = ['$injector', '$exceptionHandler', '$parse', '$browser', - function( $injector, $exceptionHandler, $parse, $browser) { + function createChildScopeClass(parent) { + function ChildScope() { + this.$$watchers = this.$$nextSibling = + this.$$childHead = this.$$childTail = null; + this.$$listeners = {}; + this.$$listenerCount = {}; + this.$$watchersCount = 0; + this.$id = nextUid(); + this.$$ChildScope = null; + } + ChildScope.prototype = parent; + return ChildScope; + } + + this.$get = ['$exceptionHandler', '$parse', '$browser', + function($exceptionHandler, $parse, $browser) { + + function destroyChildScope($event) { + $event.currentScope.$$destroyed = true; + } + + function cleanUpScope($scope) { + + // Support: IE 9 only + if (msie === 9) { + // There is a memory leak in IE9 if all child scopes are not disconnected + // completely when a scope is destroyed. So this code will recurse up through + // all this scopes children + // + // See issue https://github.com/angular/angular.js/issues/10706 + if ($scope.$$childHead) { + cleanUpScope($scope.$$childHead); + } + if ($scope.$$nextSibling) { + cleanUpScope($scope.$$nextSibling); + } + } + + // The code below works around IE9 and V8's memory leaks + // + // See: + // - https://code.google.com/p/v8/issues/detail?id=2073#c26 + // - https://github.com/angular/angular.js/issues/6794#issuecomment-38648909 + // - https://github.com/angular/angular.js/issues/1313#issuecomment-10378451 + + $scope.$parent = $scope.$$nextSibling = $scope.$$prevSibling = $scope.$$childHead = + $scope.$$childTail = $scope.$root = $scope.$$watchers = null; + } /** - * @ngdoc function - * @name ng.$rootScope.Scope + * @ngdoc type + * @name $rootScope.Scope * * @description * A root scope can be retrieved using the {@link ng.$rootScope $rootScope} key from the - * {@link AUTO.$injector $injector}. Child scopes are created using the - * {@link ng.$rootScope.Scope#methods_$new $new()} method. (Most scopes are created automatically when - * compiled HTML template is executed.) + * {@link auto.$injector $injector}. Child scopes are created using the + * {@link ng.$rootScope.Scope#$new $new()} method. (Most scopes are created automatically when + * compiled HTML template is executed.) See also the {@link guide/scope Scopes guide} for + * an in-depth introduction and usage examples. * - * Here is a simple scope snippet to show how you can interact with the scope. - *
    -         * 
    -         * 
    * * # Inheritance * A scope can inherit from a parent scope, as in this example: - *
    +         * ```js
              var parent = $rootScope;
              var child = parent.$new();
     
              parent.salutation = "Hello";
    -         child.name = "World";
              expect(child.salutation).toEqual('Hello');
     
              child.salutation = "Welcome";
              expect(child.salutation).toEqual('Welcome');
              expect(parent.salutation).toEqual('Hello');
    -         * 
    + * ``` + * + * When interacting with `Scope` in tests, additional helper methods are available on the + * instances of `Scope` type. See {@link ngMock.$rootScope.Scope ngMock Scope} for additional + * details. * * * @param {Object.=} providers Map of service factory which need to be @@ -11330,42 +17579,54 @@ function Scope() { this.$id = nextUid(); this.$$phase = this.$parent = this.$$watchers = - this.$$nextSibling = this.$$prevSibling = - this.$$childHead = this.$$childTail = null; - this['this'] = this.$root = this; + this.$$nextSibling = this.$$prevSibling = + this.$$childHead = this.$$childTail = null; + this.$root = this; this.$$destroyed = false; - this.$$asyncQueue = []; - this.$$postDigestQueue = []; this.$$listeners = {}; this.$$listenerCount = {}; - this.$$isolateBindings = {}; + this.$$watchersCount = 0; + this.$$isolateBindings = null; } /** * @ngdoc property - * @name ng.$rootScope.Scope#$id - * @propertyOf ng.$rootScope.Scope - * @returns {number} Unique scope ID (monotonically increasing alphanumeric sequence) useful for - * debugging. + * @name $rootScope.Scope#$id + * + * @description + * Unique scope ID (monotonically increasing) useful for debugging. */ + /** + * @ngdoc property + * @name $rootScope.Scope#$parent + * + * @description + * Reference to the parent scope. + */ + + /** + * @ngdoc property + * @name $rootScope.Scope#$root + * + * @description + * Reference to the root scope. + */ Scope.prototype = { constructor: Scope, /** - * @ngdoc function - * @name ng.$rootScope.Scope#$new - * @methodOf ng.$rootScope.Scope - * @function + * @ngdoc method + * @name $rootScope.Scope#$new + * @kind function * * @description * Creates a new child {@link ng.$rootScope.Scope scope}. * - * The parent scope will propagate the {@link ng.$rootScope.Scope#methods_$digest $digest()} and - * {@link ng.$rootScope.Scope#methods_$digest $digest()} events. The scope can be removed from the - * scope hierarchy using {@link ng.$rootScope.Scope#methods_$destroy $destroy()}. + * The parent scope will propagate the {@link ng.$rootScope.Scope#$digest $digest()} event. + * The scope can be removed from the scope hierarchy using {@link ng.$rootScope.Scope#$destroy $destroy()}. * - * {@link ng.$rootScope.Scope#methods_$destroy $destroy()} must be called on a scope when it is + * {@link ng.$rootScope.Scope#$destroy $destroy()} must be called on a scope when it is * desired for the scope and its child scopes to be permanently detached from the parent and * thus stop participating in model change detection and listener notification by invoking. * @@ -11374,84 +17635,96 @@ * When creating widgets, it is useful for the widget to not accidentally read parent * state. * + * @param {Scope} [parent=this] The {@link ng.$rootScope.Scope `Scope`} that will be the `$parent` + * of the newly created scope. Defaults to `this` scope if not provided. + * This is used when creating a transclude scope to correctly place it + * in the scope hierarchy while maintaining the correct prototypical + * inheritance. + * * @returns {Object} The newly created child scope. * */ - $new: function(isolate) { - var ChildScope, - child; + $new: function(isolate, parent) { + var child; + + parent = parent || this; if (isolate) { child = new Scope(); child.$root = this.$root; - // ensure that there is just one async queue per $rootScope and its children - child.$$asyncQueue = this.$$asyncQueue; - child.$$postDigestQueue = this.$$postDigestQueue; } else { - ChildScope = function() {}; // should be anonymous; This is so that when the minifier munges - // the name it does not become random set of chars. This will then show up as class - // name in the web inspector. - ChildScope.prototype = this; - child = new ChildScope(); - child.$id = nextUid(); + // Only create a child scope class if somebody asks for one, + // but cache it to allow the VM to optimize lookups. + if (!this.$$ChildScope) { + this.$$ChildScope = createChildScopeClass(this); + } + child = new this.$$ChildScope(); } - child['this'] = child; - child.$$listeners = {}; - child.$$listenerCount = {}; - child.$parent = this; - child.$$watchers = child.$$nextSibling = child.$$childHead = child.$$childTail = null; - child.$$prevSibling = this.$$childTail; - if (this.$$childHead) { - this.$$childTail.$$nextSibling = child; - this.$$childTail = child; + child.$parent = parent; + child.$$prevSibling = parent.$$childTail; + if (parent.$$childHead) { + parent.$$childTail.$$nextSibling = child; + parent.$$childTail = child; } else { - this.$$childHead = this.$$childTail = child; + parent.$$childHead = parent.$$childTail = child; } + + // When the new scope is not isolated or we inherit from `this`, and + // the parent scope is destroyed, the property `$$destroyed` is inherited + // prototypically. In all other cases, this property needs to be set + // when the parent scope is destroyed. + // The listener needs to be added after the parent is set + if (isolate || parent !== this) child.$on('$destroy', destroyChildScope); + return child; }, /** - * @ngdoc function - * @name ng.$rootScope.Scope#$watch - * @methodOf ng.$rootScope.Scope - * @function + * @ngdoc method + * @name $rootScope.Scope#$watch + * @kind function * * @description * Registers a `listener` callback to be executed whenever the `watchExpression` changes. * - * - The `watchExpression` is called on every call to {@link ng.$rootScope.Scope#methods_$digest - * $digest()} and should return the value that will be watched. (Since - * {@link ng.$rootScope.Scope#methods_$digest $digest()} reruns when it detects changes the - * `watchExpression` can execute multiple times per - * {@link ng.$rootScope.Scope#methods_$digest $digest()} and should be idempotent.) + * - The `watchExpression` is called on every call to {@link ng.$rootScope.Scope#$digest + * $digest()} and should return the value that will be watched. (`watchExpression` should not change + * its value when executed multiple times with the same input because it may be executed multiple + * times by {@link ng.$rootScope.Scope#$digest $digest()}. That is, `watchExpression` should be + * [idempotent](http://en.wikipedia.org/wiki/Idempotence).) * - The `listener` is called only when the value from the current `watchExpression` and the * previous call to `watchExpression` are not equal (with the exception of the initial run, - * see below). The inequality is determined according to - * {@link angular.equals} function. To save the value of the object for later comparison, - * the {@link angular.copy} function is used. It also means that watching complex options - * will have adverse memory and performance implications. + * see below). Inequality is determined according to reference inequality, + * [strict comparison](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Comparison_Operators) + * via the `!==` Javascript operator, unless `objectEquality == true` + * (see next point) + * - When `objectEquality == true`, inequality of the `watchExpression` is determined + * according to the {@link angular.equals} function. To save the value of the object for + * later comparison, the {@link angular.copy} function is used. This therefore means that + * watching complex objects will have adverse memory and performance implications. + * - This should not be used to watch for changes in objects that are + * or contain [File](https://developer.mozilla.org/docs/Web/API/File) objects due to limitations with {@link angular.copy `angular.copy`}. * - The watch `listener` may change the model, which may trigger other `listener`s to fire. * This is achieved by rerunning the watchers until no changes are detected. The rerun * iteration limit is 10 to prevent an infinite loop deadlock. * * - * If you want to be notified whenever {@link ng.$rootScope.Scope#methods_$digest $digest} is called, - * you can register a `watchExpression` function with no `listener`. (Since `watchExpression` - * can execute multiple times per {@link ng.$rootScope.Scope#methods_$digest $digest} cycle when a - * change is detected, be prepared for multiple calls to your listener.) + * If you want to be notified whenever {@link ng.$rootScope.Scope#$digest $digest} is called, + * you can register a `watchExpression` function with no `listener`. (Be prepared for + * multiple calls to your `watchExpression` because it will execute multiple times in a + * single {@link ng.$rootScope.Scope#$digest $digest} cycle if a change is detected.) * * After a watcher is registered with the scope, the `listener` fn is called asynchronously - * (via {@link ng.$rootScope.Scope#methods_$evalAsync $evalAsync}) to initialize the + * (via {@link ng.$rootScope.Scope#$evalAsync $evalAsync}) to initialize the * watcher. In rare cases, this is undesirable because the listener is called when the result * of `watchExpression` didn't change. To detect this scenario within the `listener` fn, you * can compare the `newVal` and `oldVal`. If these two values are identical (`===`) then the * listener was called due to initialization. * - * The example below contains an illustration of using a function as your $watch listener * * * # Example - *
    +           * ```js
                // let's assume that scope was dependency injected as the $rootScope
                var scope = $rootScope;
                scope.name = 'misko';
    @@ -11464,23 +17737,27 @@
                expect(scope.counter).toEqual(0);
     
                scope.$digest();
    -           // no variable change
    -           expect(scope.counter).toEqual(0);
    +           // the listener is always called during the first $digest loop after it was registered
    +           expect(scope.counter).toEqual(1);
    +
    +           scope.$digest();
    +           // but now it will not be called unless the value changes
    +           expect(scope.counter).toEqual(1);
     
                scope.name = 'adam';
                scope.$digest();
    -           expect(scope.counter).toEqual(1);
    +           expect(scope.counter).toEqual(2);
     
     
     
    -           // Using a listener function
    +           // Using a function as a watchExpression
                var food;
                scope.foodCounter = 0;
                expect(scope.foodCounter).toEqual(0);
                scope.$watch(
    -           // This is the listener function
    +           // This function returns the value being watched. It is called for each turn of the $digest loop
                function() { return food; },
    -           // This is the change handler
    +           // This is the change listener, called when the value returned from the above function changes
                function(newValue, oldValue) {
                    if ( newValue !== oldValue ) {
                      // Only increment the counter if the value changed
    @@ -11500,73 +17777,191 @@
                scope.$digest();
                expect(scope.foodCounter).toEqual(1);
     
    -           * 
    + * ``` * * * * @param {(function()|string)} watchExpression Expression that is evaluated on each - * {@link ng.$rootScope.Scope#methods_$digest $digest} cycle. A change in the return value triggers + * {@link ng.$rootScope.Scope#$digest $digest} cycle. A change in the return value triggers * a call to the `listener`. * * - `string`: Evaluated as {@link guide/expression expression} * - `function(scope)`: called with current `scope` as a parameter. - * @param {(function()|string)=} listener Callback called whenever the return value of - * the `watchExpression` changes. + * @param {function(newVal, oldVal, scope)} listener Callback called whenever the value + * of `watchExpression` changes. * - * - `string`: Evaluated as {@link guide/expression expression} - * - `function(newValue, oldValue, scope)`: called with current and previous values as - * parameters. - * - * @param {boolean=} objectEquality Compare object for equality rather than for reference. + * - `newVal` contains the current value of the `watchExpression` + * - `oldVal` contains the previous value of the `watchExpression` + * - `scope` refers to the current scope + * @param {boolean=} [objectEquality=false] Compare for object equality using {@link angular.equals} instead of + * comparing for reference equality. * @returns {function()} Returns a deregistration function for this listener. */ - $watch: function(watchExp, listener, objectEquality) { + $watch: function(watchExp, listener, objectEquality, prettyPrintExpression) { + var get = $parse(watchExp); + + if (get.$$watchDelegate) { + return get.$$watchDelegate(this, listener, objectEquality, get, watchExp); + } var scope = this, - get = compileToFn(watchExp, 'watch'), - array = scope.$$watchers, - watcher = { - fn: listener, - last: initWatchVal, - get: get, - exp: watchExp, - eq: !!objectEquality - }; + array = scope.$$watchers, + watcher = { + fn: listener, + last: initWatchVal, + get: get, + exp: prettyPrintExpression || watchExp, + eq: !!objectEquality + }; lastDirtyWatch = null; - // in the case user pass string, we need to compile it, do we really need this ? if (!isFunction(listener)) { - var listenFn = compileToFn(listener || noop, 'listener'); - watcher.fn = function(newVal, oldVal, scope) {listenFn(scope);}; - } - - if (typeof watchExp == 'string' && get.constant) { - var originalFn = watcher.fn; - watcher.fn = function(newVal, oldVal, scope) { - originalFn.call(this, newVal, oldVal, scope); - arrayRemove(array, watcher); - }; + watcher.fn = noop; } if (!array) { array = scope.$$watchers = []; + array.$$digestWatchIndex = -1; } // we use unshift since we use a while loop in $digest for speed. // the while loop reads in reverse order. array.unshift(watcher); + array.$$digestWatchIndex++; + incrementWatchersCount(this, 1); - return function() { - arrayRemove(array, watcher); + return function deregisterWatch() { + var index = arrayRemove(array, watcher); + if (index >= 0) { + incrementWatchersCount(scope, -1); + if (index < array.$$digestWatchIndex) { + array.$$digestWatchIndex--; + } + } lastDirtyWatch = null; }; }, + /** + * @ngdoc method + * @name $rootScope.Scope#$watchGroup + * @kind function + * + * @description + * A variant of {@link ng.$rootScope.Scope#$watch $watch()} where it watches an array of `watchExpressions`. + * If any one expression in the collection changes the `listener` is executed. + * + * - The items in the `watchExpressions` array are observed via the standard `$watch` operation. Their return + * values are examined for changes on every call to `$digest`. + * - The `listener` is called whenever any expression in the `watchExpressions` array changes. + * + * `$watchGroup` is more performant than watching each expression individually, and should be + * used when the listener does not need to know which expression has changed. + * If the listener needs to know which expression has changed, + * {@link ng.$rootScope.Scope#$watch $watch()} or + * {@link ng.$rootScope.Scope#$watchCollection $watchCollection()} should be used. + * + * @param {Array.} watchExpressions Array of expressions that will be individually + * watched using {@link ng.$rootScope.Scope#$watch $watch()} + * + * @param {function(newValues, oldValues, scope)} listener Callback called whenever the return value of any + * expression in `watchExpressions` changes + * The `newValues` array contains the current values of the `watchExpressions`, with the indexes matching + * those of `watchExpression` + * and the `oldValues` array contains the previous values of the `watchExpressions`, with the indexes matching + * those of `watchExpression`. + * + * Note that `newValues` and `oldValues` reflect the differences in each **individual** + * expression, and not the difference of the values between each call of the listener. + * That means the difference between `newValues` and `oldValues` cannot be used to determine + * which expression has changed / remained stable: + * + * ```js + * + * $scope.$watchGroup(['v1', 'v2'], function(newValues, oldValues) { + * console.log(newValues, oldValues); + * }); + * + * // newValues, oldValues initially + * // [undefined, undefined], [undefined, undefined] + * + * $scope.v1 = 'a'; + * $scope.v2 = 'a'; + * + * // ['a', 'a'], [undefined, undefined] + * + * $scope.v2 = 'b' + * + * // v1 hasn't changed since it became `'a'`, therefore its oldValue is still `undefined` + * // ['a', 'b'], [undefined, 'a'] + * + * ``` + * + * The `scope` refers to the current scope. + * @returns {function()} Returns a de-registration function for all listeners. + */ + $watchGroup: function(watchExpressions, listener) { + var oldValues = new Array(watchExpressions.length); + var newValues = new Array(watchExpressions.length); + var deregisterFns = []; + var self = this; + var changeReactionScheduled = false; + var firstRun = true; + + if (!watchExpressions.length) { + // No expressions means we call the listener ASAP + var shouldCall = true; + self.$evalAsync(function() { + if (shouldCall) listener(newValues, newValues, self); + }); + return function deregisterWatchGroup() { + shouldCall = false; + }; + } + + if (watchExpressions.length === 1) { + // Special case size of one + return this.$watch(watchExpressions[0], function watchGroupAction(value, oldValue, scope) { + newValues[0] = value; + oldValues[0] = oldValue; + listener(newValues, (value === oldValue) ? newValues : oldValues, scope); + }); + } + + forEach(watchExpressions, function(expr, i) { + var unwatchFn = self.$watch(expr, function watchGroupSubAction(value, oldValue) { + newValues[i] = value; + oldValues[i] = oldValue; + if (!changeReactionScheduled) { + changeReactionScheduled = true; + self.$evalAsync(watchGroupAction); + } + }); + deregisterFns.push(unwatchFn); + }); + + function watchGroupAction() { + changeReactionScheduled = false; + + if (firstRun) { + firstRun = false; + listener(newValues, newValues, self); + } else { + listener(newValues, oldValues, self); + } + } + + return function deregisterWatchGroup() { + while (deregisterFns.length) { + deregisterFns.shift()(); + } + }; + }, + /** - * @ngdoc function - * @name ng.$rootScope.Scope#$watchCollection - * @methodOf ng.$rootScope.Scope - * @function + * @ngdoc method + * @name $rootScope.Scope#$watchCollection + * @kind function * * @description * Shallow watches the properties of an object and fires whenever any of the properties change @@ -11580,7 +17975,7 @@ * * * # Example - *
    +           * ```js
                $scope.names = ['igor', 'matias', 'misko', 'james'];
                $scope.dataCount = 4;
     
    @@ -11599,38 +17994,53 @@
     
                //now there's been a change
                expect($scope.dataCount).toEqual(3);
    -           * 
    + * ``` * * - * @param {string|Function(scope)} obj Evaluated as {@link guide/expression expression}. The + * @param {string|function(scope)} obj Evaluated as {@link guide/expression expression}. The * expression value should evaluate to an object or an array which is observed on each - * {@link ng.$rootScope.Scope#methods_$digest $digest} cycle. Any shallow change within the + * {@link ng.$rootScope.Scope#$digest $digest} cycle. Any shallow change within the * collection will trigger a call to the `listener`. * - * @param {function(newCollection, oldCollection, scope)} listener a callback function that is - * fired with both the `newCollection` and `oldCollection` as parameters. - * The `newCollection` object is the newly modified data obtained from the `obj` expression - * and the `oldCollection` object is a copy of the former collection data. - * The `scope` refers to the current scope. + * @param {function(newCollection, oldCollection, scope)} listener a callback function called + * when a change is detected. + * - The `newCollection` object is the newly modified data obtained from the `obj` expression + * - The `oldCollection` object is a copy of the former collection data. + * Due to performance considerations, the`oldCollection` value is computed only if the + * `listener` function declares two or more arguments. + * - The `scope` argument refers to the current scope. * * @returns {function()} Returns a de-registration function for this listener. When the * de-registration function is executed, the internal watch operation is terminated. */ $watchCollection: function(obj, listener) { + $watchCollectionInterceptor.$stateful = true; + var self = this; - var oldValue; + // the current value, updated on each dirty-check run var newValue; + // a shallow copy of the newValue from the last dirty-check run, + // updated to match newValue during dirty-check run + var oldValue; + // a shallow copy of the newValue from when the last change happened + var veryOldValue; + // only track veryOldValue if the listener is asking for it + var trackVeryOldValue = (listener.length > 1); var changeDetected = 0; - var objGetter = $parse(obj); + var changeDetector = $parse(obj, $watchCollectionInterceptor); var internalArray = []; var internalObject = {}; + var initRun = true; var oldLength = 0; - function $watchCollectionWatch() { - newValue = objGetter(self); - var newLength, key; + function $watchCollectionInterceptor(_value) { + newValue = _value; + var newLength, key, bothNaN, newItem, oldItem; - if (!isObject(newValue)) { + // If the new value is undefined, then return undefined as the watch may be a one-time watch + if (isUndefined(newValue)) return; + + if (!isObject(newValue)) { // if primitive if (oldValue !== newValue) { oldValue = newValue; changeDetected++; @@ -11652,9 +18062,14 @@ } // copy the items to oldValue and look for changes. for (var i = 0; i < newLength; i++) { - if (oldValue[i] !== newValue[i]) { + oldItem = oldValue[i]; + newItem = newValue[i]; + + // eslint-disable-next-line no-self-compare + bothNaN = (oldItem !== oldItem) && (newItem !== newItem); + if (!bothNaN && (oldItem !== newItem)) { changeDetected++; - oldValue[i] = newValue[i]; + oldValue[i] = newItem; } } } else { @@ -11667,16 +18082,21 @@ // copy the items to oldValue and look for changes. newLength = 0; for (key in newValue) { - if (newValue.hasOwnProperty(key)) { + if (hasOwnProperty.call(newValue, key)) { newLength++; - if (oldValue.hasOwnProperty(key)) { - if (oldValue[key] !== newValue[key]) { + newItem = newValue[key]; + oldItem = oldValue[key]; + + if (key in oldValue) { + // eslint-disable-next-line no-self-compare + bothNaN = (oldItem !== oldItem) && (newItem !== newItem); + if (!bothNaN && (oldItem !== newItem)) { changeDetected++; - oldValue[key] = newValue[key]; + oldValue[key] = newItem; } } else { oldLength++; - oldValue[key] = newValue[key]; + oldValue[key] = newItem; changeDetected++; } } @@ -11684,8 +18104,8 @@ if (oldLength > newLength) { // we used to have more keys, need to find them and destroy them. changeDetected++; - for(key in oldValue) { - if (oldValue.hasOwnProperty(key) && !newValue.hasOwnProperty(key)) { + for (key in oldValue) { + if (!hasOwnProperty.call(newValue, key)) { oldLength--; delete oldValue[key]; } @@ -11696,40 +18116,64 @@ } function $watchCollectionAction() { - listener(newValue, oldValue, self); + if (initRun) { + initRun = false; + listener(newValue, newValue, self); + } else { + listener(newValue, veryOldValue, self); + } + + // make a copy for the next time a collection is changed + if (trackVeryOldValue) { + if (!isObject(newValue)) { + //primitive + veryOldValue = newValue; + } else if (isArrayLike(newValue)) { + veryOldValue = new Array(newValue.length); + for (var i = 0; i < newValue.length; i++) { + veryOldValue[i] = newValue[i]; + } + } else { // if object + veryOldValue = {}; + for (var key in newValue) { + if (hasOwnProperty.call(newValue, key)) { + veryOldValue[key] = newValue[key]; + } + } + } + } } - return this.$watch($watchCollectionWatch, $watchCollectionAction); + return this.$watch(changeDetector, $watchCollectionAction); }, /** - * @ngdoc function - * @name ng.$rootScope.Scope#$digest - * @methodOf ng.$rootScope.Scope - * @function + * @ngdoc method + * @name $rootScope.Scope#$digest + * @kind function * * @description - * Processes all of the {@link ng.$rootScope.Scope#methods_$watch watchers} of the current scope and - * its children. Because a {@link ng.$rootScope.Scope#methods_$watch watcher}'s listener can change - * the model, the `$digest()` keeps calling the {@link ng.$rootScope.Scope#methods_$watch watchers} + * Processes all of the {@link ng.$rootScope.Scope#$watch watchers} of the current scope and + * its children. Because a {@link ng.$rootScope.Scope#$watch watcher}'s listener can change + * the model, the `$digest()` keeps calling the {@link ng.$rootScope.Scope#$watch watchers} * until no more listeners are firing. This means that it is possible to get into an infinite * loop. This function will throw `'Maximum iteration limit exceeded.'` if the number of * iterations exceeds 10. * * Usually, you don't call `$digest()` directly in * {@link ng.directive:ngController controllers} or in - * {@link ng.$compileProvider#methods_directive directives}. - * Instead, you should call {@link ng.$rootScope.Scope#methods_$apply $apply()} (typically from within - * a {@link ng.$compileProvider#methods_directive directives}), which will force a `$digest()`. + * {@link ng.$compileProvider#directive directives}. + * Instead, you should call {@link ng.$rootScope.Scope#$apply $apply()} (typically from within + * a {@link ng.$compileProvider#directive directive}), which will force a `$digest()`. * * If you want to be notified whenever `$digest()` is called, * you can register a `watchExpression` function with - * {@link ng.$rootScope.Scope#methods_$watch $watch()} with no `listener`. + * {@link ng.$rootScope.Scope#$watch $watch()} with no `listener`. * * In unit tests, you may need to call `$digest()` to simulate the scope life cycle. * * # Example - *
    +           * ```js
                var scope = ...;
                scope.name = 'misko';
                scope.counter = 0;
    @@ -11741,27 +18185,37 @@
                expect(scope.counter).toEqual(0);
     
                scope.$digest();
    -           // no variable change
    -           expect(scope.counter).toEqual(0);
    +           // the listener is always called during the first $digest loop after it was registered
    +           expect(scope.counter).toEqual(1);
    +
    +           scope.$digest();
    +           // but now it will not be called unless the value changes
    +           expect(scope.counter).toEqual(1);
     
                scope.name = 'adam';
                scope.$digest();
    -           expect(scope.counter).toEqual(1);
    -           * 
    + expect(scope.counter).toEqual(2); + * ``` * */ $digest: function() { - var watch, value, last, - watchers, - asyncQueue = this.$$asyncQueue, - postDigestQueue = this.$$postDigestQueue, - length, - dirty, ttl = TTL, - next, current, target = this, - watchLog = [], - logIdx, logMsg, asyncTask; + var watch, value, last, fn, get, + watchers, + dirty, ttl = TTL, + next, current, target = this, + watchLog = [], + logIdx, asyncTask; beginPhase('$digest'); + // Check for changes to browser url that happened in sync before the call to $digest + $browser.$$checkUrlChange(); + + if (this === $rootScope && applyAsyncId !== null) { + // If this is the root scope, and $applyAsync has scheduled a deferred $apply(), then + // cancel the scheduled $apply and flush the queue of expressions to be evaluated. + $browser.defer.cancel(applyAsyncId); + flushApplyAsync(); + } lastDirtyWatch = null; @@ -11769,99 +18223,108 @@ dirty = false; current = target; - while(asyncQueue.length) { + // It's safe for asyncQueuePosition to be a local variable here because this loop can't + // be reentered recursively. Calling $digest from a function passed to $evalAsync would + // lead to a '$digest already in progress' error. + for (var asyncQueuePosition = 0; asyncQueuePosition < asyncQueue.length; asyncQueuePosition++) { try { - asyncTask = asyncQueue.shift(); - asyncTask.scope.$eval(asyncTask.expression); + asyncTask = asyncQueue[asyncQueuePosition]; + fn = asyncTask.fn; + fn(asyncTask.scope, asyncTask.locals); } catch (e) { - clearPhase(); $exceptionHandler(e); } lastDirtyWatch = null; } + asyncQueue.length = 0; traverseScopesLoop: - do { // "traverse the scopes" loop - if ((watchers = current.$$watchers)) { - // process our watches - length = watchers.length; - while (length--) { - try { - watch = watchers[length]; - // Most common watches are on primitives, in which case we can short - // circuit it with === operator, only when === fails do we use .equals - if (watch) { - if ((value = watch.get(current)) !== (last = watch.last) && - !(watch.eq - ? equals(value, last) - : (typeof value == 'number' && typeof last == 'number' - && isNaN(value) && isNaN(last)))) { - dirty = true; - lastDirtyWatch = watch; - watch.last = watch.eq ? copy(value) : value; - watch.fn(value, ((last === initWatchVal) ? value : last), current); - if (ttl < 5) { - logIdx = 4 - ttl; - if (!watchLog[logIdx]) watchLog[logIdx] = []; - logMsg = (isFunction(watch.exp)) - ? 'fn: ' + (watch.exp.name || watch.exp.toString()) - : watch.exp; - logMsg += '; newVal: ' + toJson(value) + '; oldVal: ' + toJson(last); - watchLog[logIdx].push(logMsg); - } - } else if (watch === lastDirtyWatch) { - // If the most recently dirty watcher is now clean, short circuit since the remaining watchers - // have already been tested. - dirty = false; - break traverseScopesLoop; + do { // "traverse the scopes" loop + if ((watchers = current.$$watchers)) { + // process our watches + watchers.$$digestWatchIndex = watchers.length; + while (watchers.$$digestWatchIndex--) { + try { + watch = watchers[watchers.$$digestWatchIndex]; + // Most common watches are on primitives, in which case we can short + // circuit it with === operator, only when === fails do we use .equals + if (watch) { + get = watch.get; + if ((value = get(current)) !== (last = watch.last) && + !(watch.eq + ? equals(value, last) + : (isNumberNaN(value) && isNumberNaN(last)))) { + dirty = true; + lastDirtyWatch = watch; + watch.last = watch.eq ? copy(value, null) : value; + fn = watch.fn; + fn(value, ((last === initWatchVal) ? value : last), current); + if (ttl < 5) { + logIdx = 4 - ttl; + if (!watchLog[logIdx]) watchLog[logIdx] = []; + watchLog[logIdx].push({ + msg: isFunction(watch.exp) ? 'fn: ' + (watch.exp.name || watch.exp.toString()) : watch.exp, + newVal: value, + oldVal: last + }); } + } else if (watch === lastDirtyWatch) { + // If the most recently dirty watcher is now clean, short circuit since the remaining watchers + // have already been tested. + dirty = false; + break traverseScopesLoop; } - } catch (e) { - clearPhase(); - $exceptionHandler(e); } + } catch (e) { + $exceptionHandler(e); } } + } - // Insanity Warning: scope depth-first traversal - // yes, this code is a bit crazy, but it works and we have tests to prove it! - // this piece should be kept in sync with the traversal in $broadcast - if (!(next = (current.$$childHead || - (current !== target && current.$$nextSibling)))) { - while(current !== target && !(next = current.$$nextSibling)) { - current = current.$parent; - } + // Insanity Warning: scope depth-first traversal + // yes, this code is a bit crazy, but it works and we have tests to prove it! + // this piece should be kept in sync with the traversal in $broadcast + if (!(next = ((current.$$watchersCount && current.$$childHead) || + (current !== target && current.$$nextSibling)))) { + while (current !== target && !(next = current.$$nextSibling)) { + current = current.$parent; } - } while ((current = next)); + } + } while ((current = next)); // `break traverseScopesLoop;` takes us to here - if(dirty && !(ttl--)) { + if ((dirty || asyncQueue.length) && !(ttl--)) { clearPhase(); throw $rootScopeMinErr('infdig', - '{0} $digest() iterations reached. Aborting!\n' + - 'Watchers fired in the last 5 iterations: {1}', - TTL, toJson(watchLog)); + '{0} $digest() iterations reached. Aborting!\n' + + 'Watchers fired in the last 5 iterations: {1}', + TTL, watchLog); } } while (dirty || asyncQueue.length); clearPhase(); - while(postDigestQueue.length) { + // postDigestQueuePosition isn't local here because this loop can be reentered recursively. + while (postDigestQueuePosition < postDigestQueue.length) { try { - postDigestQueue.shift()(); + postDigestQueue[postDigestQueuePosition++](); } catch (e) { $exceptionHandler(e); } } + postDigestQueue.length = postDigestQueuePosition = 0; + + // Check for changes to browser url that happened during the $digest + // (for which no event is fired; e.g. via `history.pushState()`) + $browser.$$checkUrlChange(); }, /** * @ngdoc event - * @name ng.$rootScope.Scope#$destroy - * @eventOf ng.$rootScope.Scope + * @name $rootScope.Scope#$destroy * @eventType broadcast on scope being destroyed * * @description @@ -11872,14 +18335,13 @@ */ /** - * @ngdoc function - * @name ng.$rootScope.Scope#$destroy - * @methodOf ng.$rootScope.Scope - * @function + * @ngdoc method + * @name $rootScope.Scope#$destroy + * @kind function * * @description * Removes the current scope (and all of its children) from the parent scope. Removal implies - * that calls to {@link ng.$rootScope.Scope#methods_$digest $digest()} will no longer + * that calls to {@link ng.$rootScope.Scope#$digest $digest()} will no longer * propagate to the current scope and its children. Removal also implies that the current * scope is eligible for garbage collection. * @@ -11895,32 +18357,44 @@ * clean up DOM bindings before an element is removed from the DOM. */ $destroy: function() { - // we can't destroy the root scope or a scope that has been already destroyed + // We can't destroy a scope that has been already destroyed. if (this.$$destroyed) return; var parent = this.$parent; this.$broadcast('$destroy'); this.$$destroyed = true; - if (this === $rootScope) return; - forEach(this.$$listenerCount, bind(null, decrementListenerCount, this)); + if (this === $rootScope) { + //Remove handlers attached to window when $rootScope is removed + $browser.$$applicationDestroyed(); + } - if (parent.$$childHead == this) parent.$$childHead = this.$$nextSibling; - if (parent.$$childTail == this) parent.$$childTail = this.$$prevSibling; + incrementWatchersCount(this, -this.$$watchersCount); + for (var eventName in this.$$listenerCount) { + decrementListenerCount(this, this.$$listenerCount[eventName], eventName); + } + + // sever all the references to parent scopes (after this cleanup, the current scope should + // not be retained by any of our references and should be eligible for garbage collection) + if (parent && parent.$$childHead === this) parent.$$childHead = this.$$nextSibling; + if (parent && parent.$$childTail === this) parent.$$childTail = this.$$prevSibling; if (this.$$prevSibling) this.$$prevSibling.$$nextSibling = this.$$nextSibling; if (this.$$nextSibling) this.$$nextSibling.$$prevSibling = this.$$prevSibling; - // This is bogus code that works around Chrome's GC leak - // see: https://github.com/angular/angular.js/issues/1313#issuecomment-10378451 - this.$parent = this.$$nextSibling = this.$$prevSibling = this.$$childHead = - this.$$childTail = null; + // Disable listeners, watchers and apply/digest methods + this.$destroy = this.$digest = this.$apply = this.$evalAsync = this.$applyAsync = noop; + this.$on = this.$watch = this.$watchGroup = function() { return noop; }; + this.$$listeners = {}; + + // Disconnect the next sibling to prevent `cleanUpScope` destroying those too + this.$$nextSibling = null; + cleanUpScope(this); }, /** - * @ngdoc function - * @name ng.$rootScope.Scope#$eval - * @methodOf ng.$rootScope.Scope - * @function + * @ngdoc method + * @name $rootScope.Scope#$eval + * @kind function * * @description * Executes the `expression` on the current scope and returns the result. Any exceptions in @@ -11928,14 +18402,14 @@ * expressions. * * # Example - *
    +           * ```js
                var scope = ng.$rootScope.Scope();
                scope.a = 1;
                scope.b = 2;
     
                expect(scope.$eval('a+b')).toEqual(3);
                expect(scope.$eval(function(scope){ return scope.a + scope.b; })).toEqual(3);
    -           * 
    + * ``` * * @param {(string|function())=} expression An angular expression to be executed. * @@ -11950,10 +18424,9 @@ }, /** - * @ngdoc function - * @name ng.$rootScope.Scope#$evalAsync - * @methodOf ng.$rootScope.Scope - * @function + * @ngdoc method + * @name $rootScope.Scope#$evalAsync + * @kind function * * @description * Executes the expression on the current scope at a later point in time. @@ -11963,7 +18436,7 @@ * * - it will execute after the function that scheduled the evaluation (preferably before DOM * rendering). - * - at least one {@link ng.$rootScope.Scope#methods_$digest $digest cycle} will be performed after + * - at least one {@link ng.$rootScope.Scope#$digest $digest cycle} will be performed after * `expression` execution. * * Any exceptions from the execution of the expression are forwarded to the @@ -11978,42 +18451,42 @@ * - `string`: execute using the rules as defined in {@link guide/expression expression}. * - `function(scope)`: execute the function with the current `scope` parameter. * + * @param {(object)=} locals Local variables object, useful for overriding values in scope. */ - $evalAsync: function(expr) { + $evalAsync: function(expr, locals) { // if we are outside of an $digest loop and this is the first time we are scheduling async // task also schedule async auto-flush - if (!$rootScope.$$phase && !$rootScope.$$asyncQueue.length) { + if (!$rootScope.$$phase && !asyncQueue.length) { $browser.defer(function() { - if ($rootScope.$$asyncQueue.length) { + if (asyncQueue.length) { $rootScope.$digest(); } }); } - this.$$asyncQueue.push({scope: this, expression: expr}); + asyncQueue.push({scope: this, fn: $parse(expr), locals: locals}); }, - $$postDigest : function(fn) { - this.$$postDigestQueue.push(fn); + $$postDigest: function(fn) { + postDigestQueue.push(fn); }, /** - * @ngdoc function - * @name ng.$rootScope.Scope#$apply - * @methodOf ng.$rootScope.Scope - * @function + * @ngdoc method + * @name $rootScope.Scope#$apply + * @kind function * * @description * `$apply()` is used to execute an expression in angular from outside of the angular * framework. (For example from browser DOM events, setTimeout, XHR or third party libraries). * Because we are calling into the angular framework we need to perform proper scope life * cycle of {@link ng.$exceptionHandler exception handling}, - * {@link ng.$rootScope.Scope#methods_$digest executing watches}. + * {@link ng.$rootScope.Scope#$digest executing watches}. * * ## Life cycle * * # Pseudo-Code of `$apply()` - *
    +           * ```js
                function $apply(expr) {
                  try {
                    return $eval(expr);
    @@ -12023,17 +18496,17 @@
                    $root.$digest();
                  }
                }
    -           * 
    + * ``` * * * Scope's `$apply()` method transitions through the following stages: * * 1. The {@link guide/expression expression} is executed using the - * {@link ng.$rootScope.Scope#methods_$eval $eval()} method. + * {@link ng.$rootScope.Scope#$eval $eval()} method. * 2. Any exceptions from the execution of the expression are forwarded to the * {@link ng.$exceptionHandler $exceptionHandler} service. - * 3. The {@link ng.$rootScope.Scope#methods_$watch watch} listeners are fired immediately after the - * expression was executed using the {@link ng.$rootScope.Scope#methods_$digest $digest()} method. + * 3. The {@link ng.$rootScope.Scope#$watch watch} listeners are fired immediately after the + * expression was executed using the {@link ng.$rootScope.Scope#$digest $digest()} method. * * * @param {(string|function())=} exp An angular expression to be executed. @@ -12046,28 +18519,61 @@ $apply: function(expr) { try { beginPhase('$apply'); - return this.$eval(expr); + try { + return this.$eval(expr); + } finally { + clearPhase(); + } } catch (e) { $exceptionHandler(e); } finally { - clearPhase(); try { $rootScope.$digest(); } catch (e) { $exceptionHandler(e); + // eslint-disable-next-line no-unsafe-finally throw e; } } }, /** - * @ngdoc function - * @name ng.$rootScope.Scope#$on - * @methodOf ng.$rootScope.Scope - * @function + * @ngdoc method + * @name $rootScope.Scope#$applyAsync + * @kind function * * @description - * Listens on events of a given type. See {@link ng.$rootScope.Scope#methods_$emit $emit} for + * Schedule the invocation of $apply to occur at a later time. The actual time difference + * varies across browsers, but is typically around ~10 milliseconds. + * + * This can be used to queue up multiple expressions which need to be evaluated in the same + * digest. + * + * @param {(string|function())=} exp An angular expression to be executed. + * + * - `string`: execute using the rules as defined in {@link guide/expression expression}. + * - `function(scope)`: execute the function with current `scope` parameter. + */ + $applyAsync: function(expr) { + var scope = this; + if (expr) { + applyAsyncQueue.push($applyAsyncExpression); + } + expr = $parse(expr); + scheduleApplyAsync(); + + function $applyAsyncExpression() { + scope.$eval(expr); + } + }, + + /** + * @ngdoc method + * @name $rootScope.Scope#$on + * @kind function + * + * @description + * Listens on events of a given type. See {@link ng.$rootScope.Scope#$emit $emit} for * discussion of event life cycle. * * The event listener function format is: `function(event, args...)`. The `event` object @@ -12075,7 +18581,8 @@ * * - `targetScope` - `{Scope}`: the scope on which the event was `$emit`-ed or * `$broadcast`-ed. - * - `currentScope` - `{Scope}`: the current scope which is handling the event. + * - `currentScope` - `{Scope}`: the scope that is currently handling the event. Once the + * event propagates through the scope hierarchy, this property is set to null. * - `name` - `{string}`: name of the event. * - `stopPropagation` - `{function=}`: calling `stopPropagation` function will cancel * further event propagation (available only for events that were `$emit`-ed). @@ -12084,7 +18591,7 @@ * - `defaultPrevented` - `{boolean}`: true if `preventDefault` was called. * * @param {string} name Event name to listen on. - * @param {function(event, args...)} listener Function to call when the event is emitted. + * @param {function(event, ...args)} listener Function to call when the event is emitted. * @returns {function()} Returns a deregistration function for this listener. */ $on: function(name, listener) { @@ -12104,56 +18611,58 @@ var self = this; return function() { - namedListeners[indexOf(namedListeners, listener)] = null; - decrementListenerCount(self, 1, name); + var indexOfListener = namedListeners.indexOf(listener); + if (indexOfListener !== -1) { + namedListeners[indexOfListener] = null; + decrementListenerCount(self, 1, name); + } }; }, /** - * @ngdoc function - * @name ng.$rootScope.Scope#$emit - * @methodOf ng.$rootScope.Scope - * @function + * @ngdoc method + * @name $rootScope.Scope#$emit + * @kind function * * @description * Dispatches an event `name` upwards through the scope hierarchy notifying the - * registered {@link ng.$rootScope.Scope#methods_$on} listeners. + * registered {@link ng.$rootScope.Scope#$on} listeners. * * The event life cycle starts at the scope on which `$emit` was called. All - * {@link ng.$rootScope.Scope#methods_$on listeners} listening for `name` event on this scope get + * {@link ng.$rootScope.Scope#$on listeners} listening for `name` event on this scope get * notified. Afterwards, the event traverses upwards toward the root scope and calls all * registered listeners along the way. The event will stop propagating if one of the listeners * cancels it. * - * Any exception emitted from the {@link ng.$rootScope.Scope#methods_$on listeners} will be passed + * Any exception emitted from the {@link ng.$rootScope.Scope#$on listeners} will be passed * onto the {@link ng.$exceptionHandler $exceptionHandler} service. * * @param {string} name Event name to emit. - * @param {...*} args Optional set of arguments which will be passed onto the event listeners. - * @return {Object} Event object (see {@link ng.$rootScope.Scope#methods_$on}). + * @param {...*} args Optional one or more arguments which will be passed onto the event listeners. + * @return {Object} Event object (see {@link ng.$rootScope.Scope#$on}). */ $emit: function(name, args) { var empty = [], - namedListeners, - scope = this, - stopPropagation = false, - event = { - name: name, - targetScope: scope, - stopPropagation: function() {stopPropagation = true;}, - preventDefault: function() { - event.defaultPrevented = true; - }, - defaultPrevented: false + namedListeners, + scope = this, + stopPropagation = false, + event = { + name: name, + targetScope: scope, + stopPropagation: function() {stopPropagation = true;}, + preventDefault: function() { + event.defaultPrevented = true; }, - listenerArgs = concat([event], arguments, 1), - i, length; + defaultPrevented: false + }, + listenerArgs = concat([event], arguments, 1), + i, length; do { namedListeners = scope.$$listeners[name] || empty; event.currentScope = scope; - for (i=0, length=namedListeners.length; i= 8 ) { - normalizedVal = urlResolve(uri).href; - if (normalizedVal !== '' && !normalizedVal.match(regex)) { - return 'unsafe:'+normalizedVal; - } + normalizedVal = urlResolve(uri).href; + if (normalizedVal !== '' && !normalizedVal.match(regex)) { + return 'unsafe:' + normalizedVal; } return uri; }; }; } + /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * Any commits to this file should be reviewed with security in mind. * + * Changes to this file can potentially create security vulnerabilities. * + * An approval from 2 Core members with history of modifying * + * this file is required. * + * * + * Does the change somehow allow for arbitrary javascript to be executed? * + * Or allows for someone to change the prototype of built-in objects? * + * Or gives undesired access to variables likes document or window? * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + + /* exported $SceProvider, $SceDelegateProvider */ + var $sceMinErr = minErr('$sce'); var SCE_CONTEXTS = { + // HTML is used when there's HTML rendered (e.g. ng-bind-html, iframe srcdoc binding). HTML: 'html', + + // Style statements or stylesheets. Currently unused in AngularJS. CSS: 'css', + + // An URL used in a context where it does not refer to a resource that loads code. Currently + // unused in AngularJS. URL: 'url', - // RESOURCE_URL is a subtype of URL used in contexts where a privileged resource is sourced from a - // url. (e.g. ng-include, script src, templateUrl) + + // RESOURCE_URL is a subtype of URL used where the referred-to resource could be interpreted as + // code. (e.g. ng-include, script src binding, templateUrl) RESOURCE_URL: 'resourceUrl', + + // Script. Currently unused in AngularJS. JS: 'js' }; // Helper functions follow. -// Copied from: -// http://docs.closure-library.googlecode.com/git/closure_goog_string_string.js.source.html#line962 -// Prereq: s is a string. - function escapeForRegexp(s) { - return s.replace(/([-()\[\]{}+?*.$\^|,:# -1) { throw $sceMinErr('iwcard', - 'Illegal sequence *** in string matcher. String: {0}', matcher); + 'Illegal sequence *** in string matcher. String: {0}', matcher); } matcher = escapeForRegexp(matcher). - replace('\\*\\*', '.*'). - replace('\\*', '[^:/.?&;]*'); + replace(/\\\*\\\*/g, '.*'). + replace(/\\\*/g, '[^:/.?&;]*'); return new RegExp('^' + matcher + '$'); } else if (isRegExp(matcher)) { // The only other type of matcher allowed is a Regexp. @@ -12412,7 +18988,7 @@ return new RegExp('^' + matcher.source + '$'); } else { throw $sceMinErr('imatcher', - 'Matchers may only be "self", string patterns or RegExp objects'); + 'Matchers may only be "self", string patterns or RegExp objects'); } } @@ -12430,13 +19006,23 @@ /** * @ngdoc service - * @name ng.$sceDelegate - * @function + * @name $sceDelegate + * @kind function * * @description * * `$sceDelegate` is a service that is used by the `$sce` service to provide {@link ng.$sce Strict - * Contextual Escaping (SCE)} services to AngularJS. + * Contextual Escaping (SCE)} services to AngularJS. + * + * For an overview of this service and the functionnality it provides in AngularJS, see the main + * page for {@link ng.$sce SCE}. The current page is targeted for developers who need to alter how + * SCE works in their application, which shouldn't be needed in most cases. + * + *
    + * AngularJS strongly relies on contextual escaping for the security of bindings: disabling or + * modifying this might cause cross site scripting (XSS) vulnerabilities. For libraries owners, + * changes to this service will also influence users, so be extra careful and document your changes. + *
    * * Typically, you would configure or override the {@link ng.$sceDelegate $sceDelegate} instead of * the `$sce` service to customize the way Strict Contextual Escaping works in AngularJS. This is @@ -12450,47 +19036,62 @@ * can override it completely to change the behavior of `$sce`, the common case would * involve configuring the {@link ng.$sceDelegateProvider $sceDelegateProvider} instead by setting * your own whitelists and blacklists for trusting URLs used for loading AngularJS resources such as - * templates. Refer {@link ng.$sceDelegateProvider#methods_resourceUrlWhitelist - * $sceDelegateProvider.resourceUrlWhitelist} and {@link - * ng.$sceDelegateProvider#methods_resourceUrlBlacklist $sceDelegateProvider.resourceUrlBlacklist} + * templates. Refer {@link ng.$sceDelegateProvider#resourceUrlWhitelist + * $sceDelegateProvider.resourceUrlWhitelist} and {@link + * ng.$sceDelegateProvider#resourceUrlBlacklist $sceDelegateProvider.resourceUrlBlacklist} */ /** - * @ngdoc object - * @name ng.$sceDelegateProvider + * @ngdoc provider + * @name $sceDelegateProvider + * @this + * * @description * * The `$sceDelegateProvider` provider allows developers to configure the {@link ng.$sceDelegate - * $sceDelegate} service. This allows one to get/set the whitelists and blacklists used to ensure - * that the URLs used for sourcing Angular templates are safe. Refer {@link - * ng.$sceDelegateProvider#methods_resourceUrlWhitelist $sceDelegateProvider.resourceUrlWhitelist} and - * {@link ng.$sceDelegateProvider#methods_resourceUrlBlacklist $sceDelegateProvider.resourceUrlBlacklist} + * $sceDelegate service}, used as a delegate for {@link ng.$sce Strict Contextual Escaping (SCE)}. + * + * The `$sceDelegateProvider` allows one to get/set the whitelists and blacklists used to ensure + * that the URLs used for sourcing AngularJS templates and other script-running URLs are safe (all + * places that use the `$sce.RESOURCE_URL` context). See + * {@link ng.$sceDelegateProvider#resourceUrlWhitelist $sceDelegateProvider.resourceUrlWhitelist} + * and + * {@link ng.$sceDelegateProvider#resourceUrlBlacklist $sceDelegateProvider.resourceUrlBlacklist}, * * For the general details about this service in Angular, read the main page for {@link ng.$sce - * Strict Contextual Escaping (SCE)}. + * Strict Contextual Escaping (SCE)}. * * **Example**: Consider the following case.
    * * - your app is hosted at url `http://myapp.example.com/` * - but some of your templates are hosted on other domains you control such as - * `http://srv01.assets.example.com/`, `http://srv02.assets.example.com/`, etc. + * `http://srv01.assets.example.com/`, `http://srv02.assets.example.com/`, etc. * - and you have an open redirect at `http://myapp.example.com/clickThru?...`. * * Here is what a secure configuration for this scenario might look like: * - *
    -   *    angular.module('myApp', []).config(function($sceDelegateProvider) {
    - *      $sceDelegateProvider.resourceUrlWhitelist([
    - *        // Allow same origin resource loads.
    - *        'self',
    - *        // Allow loading from our assets domain.  Notice the difference between * and **.
    - *        'http://srv*.assets.example.com/**']);
    +   * ```
    +   *  angular.module('myApp', []).config(function($sceDelegateProvider) {
    + *    $sceDelegateProvider.resourceUrlWhitelist([
    + *      // Allow same origin resource loads.
    + *      'self',
    + *      // Allow loading from our assets domain.  Notice the difference between * and **.
    + *      'http://srv*.assets.example.com/**'
    + *    ]);
      *
    - *      // The blacklist overrides the whitelist so the open redirect here is blocked.
    - *      $sceDelegateProvider.resourceUrlBlacklist([
    - *        'http://myapp.example.com/clickThru**']);
    - *      });
    -   * 
    + * // The blacklist overrides the whitelist so the open redirect here is blocked. + * $sceDelegateProvider.resourceUrlBlacklist([ + * 'http://myapp.example.com/clickThru**' + * ]); + * }); + * ``` + * Note that an empty whitelist will block every resource URL from being loaded, and will require + * you to manually mark each one as trusted with `$sce.trustAsResourceUrl`. However, templates + * requested by {@link ng.$templateRequest $templateRequest} that are present in + * {@link ng.$templateCache $templateCache} will not go through this check. If you have a mechanism + * to populate your templates in that cache at config time, then it is a good idea to remove 'self' + * from that whitelist. This helps to mitigate the security impact of certain types of issues, like + * for instance attacker-controlled `ng-includes`. */ function $SceDelegateProvider() { @@ -12498,32 +19099,33 @@ // Resource URLs can also be trusted by policy. var resourceUrlWhitelist = ['self'], - resourceUrlBlacklist = []; + resourceUrlBlacklist = []; /** - * @ngdoc function - * @name ng.sceDelegateProvider#resourceUrlWhitelist - * @methodOf ng.$sceDelegateProvider - * @function + * @ngdoc method + * @name $sceDelegateProvider#resourceUrlWhitelist + * @kind function * * @param {Array=} whitelist When provided, replaces the resourceUrlWhitelist with the value * provided. This must be an array or null. A snapshot of this array is used so further * changes to the array are ignored. - * * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items * allowed in this array. * - * Note: **an empty whitelist array will block all URLs**! + * @return {Array} The currently set whitelist array. * - * @return {Array} the currently set whitelist array. + * @description + * Sets/Gets the whitelist of trusted resource URLs. * * The **default value** when no whitelist has been explicitly set is `['self']` allowing only * same origin resource requests. * - * @description - * Sets/Gets the whitelist of trusted resource URLs. + *
    + * **Note:** the default whitelist of 'self' is not recommended if your app shares its origin + * with other apps! It is a good idea to limit it to only your application's directory. + *
    */ - this.resourceUrlWhitelist = function (value) { + this.resourceUrlWhitelist = function(value) { if (arguments.length) { resourceUrlWhitelist = adjustMatchers(value); } @@ -12531,34 +19133,31 @@ }; /** - * @ngdoc function - * @name ng.sceDelegateProvider#resourceUrlBlacklist - * @methodOf ng.$sceDelegateProvider - * @function + * @ngdoc method + * @name $sceDelegateProvider#resourceUrlBlacklist + * @kind function * * @param {Array=} blacklist When provided, replaces the resourceUrlBlacklist with the value * provided. This must be an array or null. A snapshot of this array is used so further - * changes to the array are ignored. - * + * changes to the array are ignored.

    * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items - * allowed in this array. - * + * allowed in this array.

    * The typical usage for the blacklist is to **block * [open redirects](http://cwe.mitre.org/data/definitions/601.html)** served by your domain as * these would otherwise be trusted but actually return content from the redirected domain. - * + *

    * Finally, **the blacklist overrides the whitelist** and has the final say. * - * @return {Array} the currently set blacklist array. - * - * The **default value** when no whitelist has been explicitly set is the empty array (i.e. there - * is no blacklist.) + * @return {Array} The currently set blacklist array. * * @description * Sets/Gets the blacklist of trusted resource URLs. + * + * The **default value** when no whitelist has been explicitly set is the empty array (i.e. there + * is no blacklist.) */ - this.resourceUrlBlacklist = function (value) { + this.resourceUrlBlacklist = function(value) { if (arguments.length) { resourceUrlBlacklist = adjustMatchers(value); } @@ -12626,7 +19225,7 @@ } var trustedValueHolderBase = generateHolderType(), - byType = {}; + byType = {}; byType[SCE_CONTEXTS.HTML] = generateHolderType(trustedValueHolderBase); byType[SCE_CONTEXTS.CSS] = generateHolderType(trustedValueHolderBase); @@ -12636,59 +19235,64 @@ /** * @ngdoc method - * @name ng.$sceDelegate#trustAs - * @methodOf ng.$sceDelegate + * @name $sceDelegate#trustAs * * @description - * Returns an object that is trusted by angular for use in specified strict - * contextual escaping contexts (such as ng-html-bind-unsafe, ng-include, any src - * attribute interpolation, any dom event binding attribute interpolation - * such as for onclick, etc.) that uses the provided value. - * See {@link ng.$sce $sce} for enabling strict contextual escaping. + * Returns a trusted representation of the parameter for the specified context. This trusted + * object will later on be used as-is, without any security check, by bindings or directives + * that require this security context. + * For instance, marking a string as trusted for the `$sce.HTML` context will entirely bypass + * the potential `$sanitize` call in corresponding `$sce.HTML` bindings or directives, such as + * `ng-bind-html`. Note that in most cases you won't need to call this function: if you have the + * sanitizer loaded, passing the value itself will render all the HTML that does not pose a + * security risk. * - * @param {string} type The kind of context in which this value is safe for use. e.g. url, - * resourceUrl, html, js and css. - * @param {*} value The value that that should be considered trusted/safe. - * @returns {*} A value that can be used to stand in for the provided `value` in places - * where Angular expects a $sce.trustAs() return value. + * See {@link ng.$sceDelegate#getTrusted getTrusted} for the function that will consume those + * trusted values, and {@link ng.$sce $sce} for general documentation about strict contextual + * escaping. + * + * @param {string} type The context in which this value is safe for use, e.g. `$sce.URL`, + * `$sce.RESOURCE_URL`, `$sce.HTML`, `$sce.JS` or `$sce.CSS`. + * + * @param {*} value The value that should be considered trusted. + * @return {*} A trusted representation of value, that can be used in the given context. */ function trustAs(type, trustedValue) { var Constructor = (byType.hasOwnProperty(type) ? byType[type] : null); if (!Constructor) { throw $sceMinErr('icontext', - 'Attempted to trust a value in invalid context. Context: {0}; Value: {1}', - type, trustedValue); + 'Attempted to trust a value in invalid context. Context: {0}; Value: {1}', + type, trustedValue); } - if (trustedValue === null || trustedValue === undefined || trustedValue === '') { + if (trustedValue === null || isUndefined(trustedValue) || trustedValue === '') { return trustedValue; } // All the current contexts in SCE_CONTEXTS happen to be strings. In order to avoid trusting // mutable objects, we ensure here that the value passed in is actually a string. if (typeof trustedValue !== 'string') { throw $sceMinErr('itype', - 'Attempted to trust a non-string value in a content requiring a string: Context: {0}', - type); + 'Attempted to trust a non-string value in a content requiring a string: Context: {0}', + type); } return new Constructor(trustedValue); } /** * @ngdoc method - * @name ng.$sceDelegate#valueOf - * @methodOf ng.$sceDelegate + * @name $sceDelegate#valueOf * * @description - * If the passed parameter had been returned by a prior call to {@link ng.$sceDelegate#methods_trustAs - * `$sceDelegate.trustAs`}, returns the value that had been passed to {@link - * ng.$sceDelegate#methods_trustAs `$sceDelegate.trustAs`}. + * If the passed parameter had been returned by a prior call to {@link ng.$sceDelegate#trustAs + * `$sceDelegate.trustAs`}, returns the value that had been passed to {@link + * ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}. * * If the passed parameter is not a value that had been returned by {@link - * ng.$sceDelegate#methods_trustAs `$sceDelegate.trustAs`}, returns it as-is. + * ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}, it must be returned as-is. * - * @param {*} value The result of a prior {@link ng.$sceDelegate#methods_trustAs `$sceDelegate.trustAs`} - * call or anything else. - * @returns {*} The value the was originally provided to {@link ng.$sceDelegate#methods_trustAs - * `$sceDelegate.trustAs`} if `value` is the result of such a call. Otherwise, returns + * @param {*} value The result of a prior {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs`} + * call or anything else. + * @return {*} The `value` that was originally provided to {@link ng.$sceDelegate#trustAs + * `$sceDelegate.trustAs`} if `value` is the result of such a call. Otherwise, returns * `value` unchanged. */ function valueOf(maybeTrusted) { @@ -12701,42 +19305,53 @@ /** * @ngdoc method - * @name ng.$sceDelegate#getTrusted - * @methodOf ng.$sceDelegate + * @name $sceDelegate#getTrusted * * @description - * Takes the result of a {@link ng.$sceDelegate#methods_trustAs `$sceDelegate.trustAs`} call and - * returns the originally supplied value if the queried context type is a supertype of the - * created type. If this condition isn't satisfied, throws an exception. + * Takes any input, and either returns a value that's safe to use in the specified context, or + * throws an exception. * - * @param {string} type The kind of context in which this value is to be used. - * @param {*} maybeTrusted The result of a prior {@link ng.$sceDelegate#methods_trustAs - * `$sceDelegate.trustAs`} call. - * @returns {*} The value the was originally provided to {@link ng.$sceDelegate#methods_trustAs - * `$sceDelegate.trustAs`} if valid in this context. Otherwise, throws an exception. + * In practice, there are several cases. When given a string, this function runs checks + * and sanitization to make it safe without prior assumptions. When given the result of a {@link + * ng.$sceDelegate#trustAs `$sceDelegate.trustAs`} call, it returns the originally supplied + * value if that value's context is valid for this call's context. Finally, this function can + * also throw when there is no way to turn `maybeTrusted` in a safe value (e.g., no sanitization + * is available or possible.) + * + * @param {string} type The context in which this value is to be used (such as `$sce.HTML`). + * @param {*} maybeTrusted The result of a prior {@link ng.$sceDelegate#trustAs + * `$sceDelegate.trustAs`} call, or anything else (which will not be considered trusted.) + * @return {*} A version of the value that's safe to use in the given context, or throws an + * exception if this is impossible. */ function getTrusted(type, maybeTrusted) { - if (maybeTrusted === null || maybeTrusted === undefined || maybeTrusted === '') { + if (maybeTrusted === null || isUndefined(maybeTrusted) || maybeTrusted === '') { return maybeTrusted; } var constructor = (byType.hasOwnProperty(type) ? byType[type] : null); + // If maybeTrusted is a trusted class instance or subclass instance, then unwrap and return + // as-is. if (constructor && maybeTrusted instanceof constructor) { return maybeTrusted.$$unwrapTrustedValue(); } - // If we get here, then we may only take one of two actions. - // 1. sanitize the value for the requested type, or - // 2. throw an exception. + // Otherwise, if we get here, then we may either make it safe, or throw an exception. This + // depends on the context: some are sanitizatible (HTML), some use whitelists (RESOURCE_URL), + // some are impossible to do (JS). This step isn't implemented for CSS and URL, as AngularJS + // has no corresponding sinks. if (type === SCE_CONTEXTS.RESOURCE_URL) { + // RESOURCE_URL uses a whitelist. if (isResourceUrlAllowedByPolicy(maybeTrusted)) { return maybeTrusted; } else { throw $sceMinErr('insecurl', - 'Blocked loading resource from url not allowed by $sceDelegate policy. URL: {0}', - maybeTrusted.toString()); + 'Blocked loading resource from url not allowed by $sceDelegate policy. URL: {0}', + maybeTrusted.toString()); } } else if (type === SCE_CONTEXTS.HTML) { + // htmlSanitizer throws its own error when no sanitizer is available. return htmlSanitizer(maybeTrusted); } + // Default error when the $sce service has no way to make the input safe. throw $sceMinErr('unsafe', 'Attempting to use an unsafe value in a safe context.'); } @@ -12748,8 +19363,10 @@ /** - * @ngdoc object - * @name ng.$sceProvider + * @ngdoc provider + * @name $sceProvider + * @this + * * @description * * The $sceProvider provider allows developers to configure the {@link ng.$sce $sce} service. @@ -12759,12 +19376,10 @@ * Read more about {@link ng.$sce Strict Contextual Escaping (SCE)}. */ - /* jshint maxlen: false*/ - /** * @ngdoc service - * @name ng.$sce - * @function + * @name $sce + * @kind function * * @description * @@ -12772,34 +19387,40 @@ * * # Strict Contextual Escaping * - * Strict Contextual Escaping (SCE) is a mode in which AngularJS requires bindings in certain - * contexts to result in a value that is marked as safe to use for that context. One example of - * such a context is binding arbitrary html controlled by the user via `ng-bind-html`. We refer - * to these contexts as privileged or SCE contexts. + * Strict Contextual Escaping (SCE) is a mode in which AngularJS constrains bindings to only render + * trusted values. Its goal is to assist in writing code in a way that (a) is secure by default, and + * (b) makes auditing for security vulnerabilities such as XSS, clickjacking, etc. a lot easier. * - * As of version 1.2, Angular ships with SCE enabled by default. + * ## Overview * - * Note: When enabled (the default), IE8 in quirks mode is not supported. In this mode, IE8 allows - * one to execute arbitrary javascript by the use of the expression() syntax. Refer - * to learn more about them. - * You can ensure your document is in standards mode and not quirks mode by adding `` - * to the top of your HTML document. + * To systematically block XSS security bugs, AngularJS treats all values as untrusted by default in + * HTML or sensitive URL bindings. When binding untrusted values, AngularJS will automatically + * run security checks on them (sanitizations, whitelists, depending on context), or throw when it + * cannot guarantee the security of the result. That behavior depends strongly on contexts: HTML + * can be sanitized, but template URLs cannot, for instance. * - * SCE assists in writing code in way that (a) is secure by default and (b) makes auditing for - * security vulnerabilities such as XSS, clickjacking, etc. a lot easier. + * To illustrate this, consider the `ng-bind-html` directive. It renders its value directly as HTML: + * we call that the *context*. When given an untrusted input, AngularJS will attempt to sanitize it + * before rendering if a sanitizer is available, and throw otherwise. To bypass sanitization and + * render the input as-is, you will need to mark it as trusted for that context before attempting + * to bind it. + * + * As of version 1.2, AngularJS ships with SCE enabled by default. + * + * ## In practice * * Here's an example of a binding in a privileged context: * - *

    -   *     
    -   *     
    - *
    + * ``` + * + *
    + * ``` * * Notice that `ng-bind-html` is bound to `userHtml` controlled by the user. With SCE - * disabled, this application allows the user to render arbitrary HTML into the DIV. - * In a more realistic example, one may be rendering user comments, blog articles, etc. via - * bindings. (HTML is just one example of a context where rendering user controlled input creates - * security vulnerabilities.) + * disabled, this application allows the user to render arbitrary HTML into the DIV, which would + * be an XSS security bug. In a more realistic example, one may be rendering user comments, blog + * articles, etc. via bindings. (HTML is just one example of a context where rendering user + * controlled input creates security vulnerabilities.) * * For the case of HTML, you might use a library, either on the client side, or on the server side, * to sanitize unsafe HTML before binding to the value and rendering it in the document. @@ -12809,39 +19430,43 @@ * ensure that you didn't accidentally delete the line that sanitized the value, or renamed some * properties/fields and forgot to update the binding to the sanitized value? * - * To be secure by default, you want to ensure that any such bindings are disallowed unless you can - * determine that something explicitly says it's safe to use a value for binding in that - * context. You can then audit your code (a simple grep would do) to ensure that this is only done - * for those values that you can easily tell are safe - because they were received from your server, - * sanitized by your library, etc. You can organize your codebase to help with this - perhaps - * allowing only the files in a specific directory to do this. Ensuring that the internal API - * exposed by that code doesn't markup arbitrary values as safe then becomes a more manageable task. - * - * In the case of AngularJS' SCE service, one uses {@link ng.$sce#methods_trustAs $sce.trustAs} - * (and shorthand methods such as {@link ng.$sce#methods_trustAsHtml $sce.trustAsHtml}, etc.) to - * obtain values that will be accepted by SCE / privileged contexts. + * To be secure by default, AngularJS makes sure bindings go through that sanitization, or + * any similar validation process, unless there's a good reason to trust the given value in this + * context. That trust is formalized with a function call. This means that as a developer, you + * can assume all untrusted bindings are safe. Then, to audit your code for binding security issues, + * you just need to ensure the values you mark as trusted indeed are safe - because they were + * received from your server, sanitized by your library, etc. You can organize your codebase to + * help with this - perhaps allowing only the files in a specific directory to do this. + * Ensuring that the internal API exposed by that code doesn't markup arbitrary values as safe then + * becomes a more manageable task. * + * In the case of AngularJS' SCE service, one uses {@link ng.$sce#trustAs $sce.trustAs} + * (and shorthand methods such as {@link ng.$sce#trustAsHtml $sce.trustAsHtml}, etc.) to + * build the trusted versions of your values. * * ## How does it work? * - * In privileged contexts, directives and code will bind to the result of {@link ng.$sce#methods_getTrusted - * $sce.getTrusted(context, value)} rather than to the value directly. Directives use {@link - * ng.$sce#methods_parse $sce.parseAs} rather than `$parse` to watch attribute bindings, which performs the - * {@link ng.$sce#methods_getTrusted $sce.getTrusted} behind the scenes on non-constant literals. + * In privileged contexts, directives and code will bind to the result of {@link ng.$sce#getTrusted + * $sce.getTrusted(context, value)} rather than to the value directly. Think of this function as + * a way to enforce the required security context in your data sink. Directives use {@link + * ng.$sce#parseAs $sce.parseAs} rather than `$parse` to watch attribute bindings, which performs + * the {@link ng.$sce#getTrusted $sce.getTrusted} behind the scenes on non-constant literals. Also, + * when binding without directives, AngularJS will understand the context of your bindings + * automatically. * * As an example, {@link ng.directive:ngBindHtml ngBindHtml} uses {@link - * ng.$sce#methods_parseAsHtml $sce.parseAsHtml(binding expression)}. Here's the actual code (slightly + * ng.$sce#parseAsHtml $sce.parseAsHtml(binding expression)}. Here's the actual code (slightly * simplified): * - *
    -   *   var ngBindHtmlDirective = ['$sce', function($sce) {
    - *     return function(scope, element, attr) {
    - *       scope.$watch($sce.parseAsHtml(attr.ngBindHtml), function(value) {
    - *         element.html(value || '');
    - *       });
    - *     };
    - *   }];
    -   * 
    + * ``` + * var ngBindHtmlDirective = ['$sce', function($sce) { + * return function(scope, element, attr) { + * scope.$watch($sce.parseAsHtml(attr.ngBindHtml), function(value) { + * element.html(value || ''); + * }); + * }; + * }]; + * ``` * * ## Impact on loading templates * @@ -12849,37 +19474,38 @@ * `templateUrl`'s specified by {@link guide/directive directives}. * * By default, Angular only loads templates from the same domain and protocol as the application - * document. This is done by calling {@link ng.$sce#methods_getTrustedResourceUrl - * $sce.getTrustedResourceUrl} on the template URL. To load templates from other domains and/or - * protocols, you may either either {@link ng.$sceDelegateProvider#methods_resourceUrlWhitelist whitelist - * them} or {@link ng.$sce#methods_trustAsResourceUrl wrap it} into a trusted value. + * document. This is done by calling {@link ng.$sce#getTrustedResourceUrl + * $sce.getTrustedResourceUrl} on the template URL. To load templates from other domains and/or + * protocols, you may either {@link ng.$sceDelegateProvider#resourceUrlWhitelist whitelist + * them} or {@link ng.$sce#trustAsResourceUrl wrap it} into a trusted value. * * *Please note*: * 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)} + * [Same Origin Policy](https://code.google.com/p/browsersec/wiki/Part2#Same-origin_policy_for_XMLHttpRequest) + * and [Cross-Origin Resource Sharing (CORS)](http://www.w3.org/TR/cors/) * policy apply in addition to this and may further restrict whether the template is successfully * loaded. This means that without the right CORS policy, loading templates from a different domain * won't work on all browsers. Also, loading templates from `file://` URL does not work on some * browsers. * - * ## This feels like too much overhead for the developer? + * ## This feels like too much overhead * * It's important to remember that SCE only applies to interpolation expressions. * * If your expressions are constant literals, they're automatically trusted and you don't need to - * call `$sce.trustAs` on them. (e.g. - * `
    `) just works. - * - * Additionally, `a[href]` and `img[src]` automatically sanitize their URLs and do not pass them - * through {@link ng.$sce#methods_getTrusted $sce.getTrusted}. SCE doesn't play a role here. + * call `$sce.trustAs` on them (e.g. + * `
    `) just works. The `$sceDelegate` will + * also use the `$sanitize` service if it is available when binding untrusted values to + * `$sce.HTML` context. AngularJS provides an implementation in `angular-sanitize.js`, and if you + * wish to use it, you will also need to depend on the {@link ngSanitize `ngSanitize`} module in + * your application. * * The included {@link ng.$sceDelegate $sceDelegate} comes with sane defaults to allow you to load * templates in `ng-include` from your application's domain without having to even know about SCE. * It blocks loading templates from other domains or loading templates over http from an https * served document. You can change these by setting your own custom {@link - * ng.$sceDelegateProvider#methods_resourceUrlWhitelist whitelists} and {@link - * ng.$sceDelegateProvider#methods_resourceUrlBlacklist blacklists} for matching such URLs. + * ng.$sceDelegateProvider#resourceUrlWhitelist whitelists} and {@link + * ng.$sceDelegateProvider#resourceUrlBlacklist blacklists} for matching such URLs. * * This significantly reduces the overhead. It is far easier to pay the small overhead and have an * application that's secure and can be audited to verify that with much more ease than bolting @@ -12890,13 +19516,19 @@ * * | Context | Notes | * |---------------------|----------------| - * | `$sce.HTML` | For HTML that's safe to source into the application. The {@link ng.directive:ngBindHtml ngBindHtml} directive uses this context for bindings. | - * | `$sce.CSS` | For CSS that's safe to source into the application. Currently unused. Feel free to use it in your own directives. | - * | `$sce.URL` | For URLs that are safe to follow as links. Currently unused (`
    Note that `$sce.RESOURCE_URL` makes a stronger statement about the URL than `$sce.URL` does and therefore contexts requiring values trusted for `$sce.RESOURCE_URL` can be used anywhere that values trusted for `$sce.URL` are required. | - * | `$sce.JS` | For JavaScript that is safe to execute in your application's context. Currently unused. Feel free to use it in your own directives. | + * | `$sce.HTML` | For HTML that's safe to source into the application. The {@link ng.directive:ngBindHtml ngBindHtml} directive uses this context for bindings. If an unsafe value is encountered, and the {@link ngSanitize.$sanitize $sanitize} service is available (implemented by the {@link ngSanitize ngSanitize} module) this will sanitize the value instead of throwing an error. | + * | `$sce.CSS` | For CSS that's safe to source into the application. Currently, no bindings require this context. Feel free to use it in your own directives. | + * | `$sce.URL` | For URLs that are safe to follow as links. Currently unused (`

    Note that `$sce.RESOURCE_URL` makes a stronger statement about the URL than `$sce.URL` does (it's not just the URL that matters, but also what is at the end of it), and therefore contexts requiring values trusted for `$sce.RESOURCE_URL` can be used anywhere that values trusted for `$sce.URL` are required. | + * | `$sce.JS` | For JavaScript that is safe to execute in your application's context. Currently, no bindings require this context. Feel free to use it in your own directives. | * - * ## Format of items in {@link ng.$sceDelegateProvider#methods_resourceUrlWhitelist resourceUrlWhitelist}/{@link ng.$sceDelegateProvider#methods_resourceUrlBlacklist Blacklist}
    + * + * Be aware that `a[href]` and `img[src]` automatically sanitize their URLs and do not pass them + * through {@link ng.$sce#getTrusted $sce.getTrusted}. There's no CSS-, URL-, or JS-context bindings + * in AngularJS currently, so their corresponding `$sce.trustAs` functions aren't useful yet. This + * might evolve. + * + * ## Format of items in {@link ng.$sceDelegateProvider#resourceUrlWhitelist resourceUrlWhitelist}/{@link ng.$sceDelegateProvider#resourceUrlBlacklist Blacklist} * * Each element in these arrays must be one of the following: * @@ -12908,33 +19540,33 @@ * being tested (substring matches are not good enough.) * - There are exactly **two wildcard sequences** - `*` and `**`. All other characters * match themselves. - * - `*`: matches zero or more occurances of any character other than one of the following 6 - * characters: '`:`', '`/`', '`.`', '`?`', '`&`' and ';'. It's a useful wildcard for use + * - `*`: matches zero or more occurrences of any character other than one of the following 6 + * characters: '`:`', '`/`', '`.`', '`?`', '`&`' and '`;`'. It's a useful wildcard for use * in a whitelist. - * - `**`: matches zero or more occurances of *any* character. As such, it's not - * not appropriate to use in for a scheme, domain, etc. as it would match too much. (e.g. + * - `**`: matches zero or more occurrences of *any* character. As such, it's not + * appropriate for use in a scheme, domain, etc. as it would match too much. (e.g. * http://**.example.com/ would match http://evil.com/?ignore=.example.com/ and that might - * not have been the intention.) It's usage at the very end of the path is ok. (e.g. + * not have been the intention.) Its usage at the very end of the path is ok. (e.g. * http://foo.example.com/templates/**). * - **RegExp** (*see caveat below*) * - *Caveat*: While regular expressions are powerful and offer great flexibility, their syntax * (and all the inevitable escaping) makes them *harder to maintain*. It's easy to * accidentally introduce a bug when one updates a complex expression (imho, all regexes should - * have good test coverage.). For instance, the use of `.` in the regex is correct only in a + * have good test coverage). For instance, the use of `.` in the regex is correct only in a * small number of cases. A `.` character in the regex used when matching the scheme or a * subdomain could be matched against a `:` or literal `.` that was likely not intended. It * is highly recommended to use the string patterns and only fall back to regular expressions - * if they as a last resort. + * as a last resort. * - The regular expression must be an instance of RegExp (i.e. not a string.) It is * matched against the **entire** *normalized / absolute URL* of the resource being tested * (even when the RegExp did not have the `^` and `$` codes.) In addition, any flags * present on the RegExp (such as multiline, global, ignoreCase) are ignored. - * - If you are generating your Javascript from some other templating engine (not + * - If you are generating your JavaScript from some other templating engine (not * recommended, e.g. in issue [#4006](https://github.com/angular/angular.js/issues/4006)), * remember to escape your regular expression (and be aware that you might need more than * one level of escaping depending on your templating engine and the way you interpolated * the value.) Do make use of your platform's escaping mechanism as it might be good - * enough before coding your own. e.g. Ruby has + * enough before coding your own. E.g. Ruby has * [Regexp.escape(str)](http://www.ruby-doc.org/core-2.0.0/Regexp.html#method-c-escape) * and Python has [re.escape](http://docs.python.org/library/re.html#re.escape). * Javascript lacks a similar built in function for escaping. Take a look at Google @@ -12945,64 +19577,65 @@ * * ## Show me an example using SCE. * - * @example - - -
    -

    - User comments
    - By default, HTML that isn't explicitly trusted (e.g. Alice's comment) is sanitized when - $sanitize is available. If $sanitize isn't available, this results in an error instead of an - exploit. -
    -
    - {{userComment.name}}: - -
    -
    -
    -
    -
    - - - var mySceApp = angular.module('mySceApp', ['ngSanitize']); - - mySceApp.controller("myAppController", function myAppController($http, $templateCache, $sce) { - var self = this; - $http.get("test_data.json", {cache: $templateCache}).success(function(userComments) { - self.userComments = userComments; - }); - self.explicitlyTrustedHtml = $sce.trustAsHtml( - 'Hover over this text.'); - }); - - - - [ - { "name": "Alice", - "htmlComment": - "Is anyone reading this?" - }, - { "name": "Bob", - "htmlComment": "Yes! Am I the only other one?" - } - ] - - - - describe('SCE doc demo', function() { - it('should sanitize untrusted values', function() { - expect(element('.htmlComment').html()).toBe('Is anyone reading this?'); - }); - it('should NOT sanitize explicitly trusted values', function() { - expect(element('#explicitlyTrustedHtml').html()).toBe( - 'Hover over this text.'); - }); - }); - -
    + * + * + *
    + *

    + * User comments
    + * By default, HTML that isn't explicitly trusted (e.g. Alice's comment) is sanitized when + * $sanitize is available. If $sanitize isn't available, this results in an error instead of an + * exploit. + *
    + *
    + * {{userComment.name}}: + * + *
    + *
    + *
    + *
    + *
    + * + * + * angular.module('mySceApp', ['ngSanitize']) + * .controller('AppController', ['$http', '$templateCache', '$sce', + * function AppController($http, $templateCache, $sce) { + * var self = this; + * $http.get('test_data.json', {cache: $templateCache}).then(function(response) { + * self.userComments = response.data; + * }); + * self.explicitlyTrustedHtml = $sce.trustAsHtml( + * 'Hover over this text.'); + * }]); + * + * + * + * [ + * { "name": "Alice", + * "htmlComment": + * "Is anyone reading this?" + * }, + * { "name": "Bob", + * "htmlComment": "Yes! Am I the only other one?" + * } + * ] + * + * + * + * describe('SCE doc demo', function() { + * it('should sanitize untrusted values', function() { + * expect(element.all(by.css('.htmlComment')).first().getAttribute('innerHTML')) + * .toBe('Is anyone reading this?'); + * }); + * + * it('should NOT sanitize explicitly trusted values', function() { + * expect(element(by.id('explicitlyTrustedHtml')).getAttribute('innerHTML')).toBe( + * 'Hover over this text.'); + * }); + * }); + * + *
    * * * @@ -13012,37 +19645,36 @@ * for little coding overhead. It will be much harder to take an SCE disabled application and * either secure it on your own or enable SCE at a later stage. It might make sense to disable SCE * for cases where you have a lot of existing code that was written before SCE was introduced and - * you're migrating them a module at a time. + * you're migrating them a module at a time. Also do note that this is an app-wide setting, so if + * you are writing a library, you will cause security bugs applications using it. * * That said, here's how you can completely disable SCE: * - *
    -   *   angular.module('myAppWithSceDisabledmyApp', []).config(function($sceProvider) {
    - *     // Completely disable SCE.  For demonstration purposes only!
    - *     // Do not use in new projects.
    - *     $sceProvider.enabled(false);
    - *   });
    -   * 
    + * ``` + * angular.module('myAppWithSceDisabledmyApp', []).config(function($sceProvider) { + * // Completely disable SCE. For demonstration purposes only! + * // Do not use in new projects or libraries. + * $sceProvider.enabled(false); + * }); + * ``` * */ - /* jshint maxlen: 100 */ function $SceProvider() { var enabled = true; /** - * @ngdoc function - * @name ng.sceProvider#enabled - * @methodOf ng.$sceProvider - * @function + * @ngdoc method + * @name $sceProvider#enabled + * @kind function * - * @param {boolean=} value If provided, then enables/disables SCE. - * @return {boolean} true if SCE is enabled, false otherwise. + * @param {boolean=} value If provided, then enables/disables SCE application-wide. + * @return {boolean} True if SCE is enabled, false otherwise. * * @description * Enables/disables SCE and returns the current value. */ - this.enabled = function (value) { + this.enabled = function(value) { if (arguments.length) { enabled = !!value; } @@ -13051,77 +19683,77 @@ /* Design notes on the default implementation for SCE. - * - * The API contract for the SCE delegate - * ------------------------------------- - * The SCE delegate object must provide the following 3 methods: - * - * - trustAs(contextEnum, value) - * This method is used to tell the SCE service that the provided value is OK to use in the - * contexts specified by contextEnum. It must return an object that will be accepted by - * getTrusted() for a compatible contextEnum and return this value. - * - * - valueOf(value) - * For values that were not produced by trustAs(), return them as is. For values that were - * produced by trustAs(), return the corresponding input value to trustAs. Basically, if - * trustAs is wrapping the given values into some type, this operation unwraps it when given - * such a value. - * - * - getTrusted(contextEnum, value) - * This function should return the a value that is safe to use in the context specified by - * contextEnum or throw and exception otherwise. - * - * NOTE: This contract deliberately does NOT state that values returned by trustAs() must be - * opaque or wrapped in some holder object. That happens to be an implementation detail. For - * instance, an implementation could maintain a registry of all trusted objects by context. In - * such a case, trustAs() would return the same object that was passed in. getTrusted() would - * return the same object passed in if it was found in the registry under a compatible context or - * throw an exception otherwise. An implementation might only wrap values some of the time based - * on some criteria. getTrusted() might return a value and not throw an exception for special - * constants or objects even if not wrapped. All such implementations fulfill this contract. - * - * - * A note on the inheritance model for SCE contexts - * ------------------------------------------------ - * I've used inheritance and made RESOURCE_URL wrapped types a subtype of URL wrapped types. This - * is purely an implementation details. - * - * The contract is simply this: - * - * getTrusted($sce.RESOURCE_URL, value) succeeding implies that getTrusted($sce.URL, value) - * will also succeed. - * - * Inheritance happens to capture this in a natural way. In some future, we - * may not use inheritance anymore. That is OK because no code outside of - * sce.js and sceSpecs.js would need to be aware of this detail. - */ + * + * The API contract for the SCE delegate + * ------------------------------------- + * The SCE delegate object must provide the following 3 methods: + * + * - trustAs(contextEnum, value) + * This method is used to tell the SCE service that the provided value is OK to use in the + * contexts specified by contextEnum. It must return an object that will be accepted by + * getTrusted() for a compatible contextEnum and return this value. + * + * - valueOf(value) + * For values that were not produced by trustAs(), return them as is. For values that were + * produced by trustAs(), return the corresponding input value to trustAs. Basically, if + * trustAs is wrapping the given values into some type, this operation unwraps it when given + * such a value. + * + * - getTrusted(contextEnum, value) + * This function should return the a value that is safe to use in the context specified by + * contextEnum or throw and exception otherwise. + * + * NOTE: This contract deliberately does NOT state that values returned by trustAs() must be + * opaque or wrapped in some holder object. That happens to be an implementation detail. For + * instance, an implementation could maintain a registry of all trusted objects by context. In + * such a case, trustAs() would return the same object that was passed in. getTrusted() would + * return the same object passed in if it was found in the registry under a compatible context or + * throw an exception otherwise. An implementation might only wrap values some of the time based + * on some criteria. getTrusted() might return a value and not throw an exception for special + * constants or objects even if not wrapped. All such implementations fulfill this contract. + * + * + * A note on the inheritance model for SCE contexts + * ------------------------------------------------ + * I've used inheritance and made RESOURCE_URL wrapped types a subtype of URL wrapped types. This + * is purely an implementation details. + * + * The contract is simply this: + * + * getTrusted($sce.RESOURCE_URL, value) succeeding implies that getTrusted($sce.URL, value) + * will also succeed. + * + * Inheritance happens to capture this in a natural way. In some future, we may not use + * inheritance anymore. That is OK because no code outside of sce.js and sceSpecs.js would need to + * be aware of this detail. + */ - this.$get = ['$parse', '$sniffer', '$sceDelegate', function( - $parse, $sniffer, $sceDelegate) { - // Prereq: Ensure that we're not running in IE8 quirks mode. In that mode, IE allows + this.$get = ['$parse', '$sceDelegate', function( + $parse, $sceDelegate) { + // Support: IE 9-11 only + // Prereq: Ensure that we're not running in IE<11 quirks mode. In that mode, IE < 11 allow // the "expression(javascript expression)" syntax which is insecure. - if (enabled && $sniffer.msie && $sniffer.msieDocumentMode < 8) { + if (enabled && msie < 8) { throw $sceMinErr('iequirks', - 'Strict Contextual Escaping does not support Internet Explorer version < 9 in quirks ' + - 'mode. You can fix this by adding the text to the top of your HTML ' + - 'document. See http://docs.angularjs.org/../api/ng.$sce for more information.'); + 'Strict Contextual Escaping does not support Internet Explorer version < 11 in quirks ' + + 'mode. You can fix this by adding the text to the top of your HTML ' + + 'document. See http://docs.angularjs.org/api/ng.$sce for more information.'); } - var sce = copy(SCE_CONTEXTS); + var sce = shallowCopy(SCE_CONTEXTS); /** - * @ngdoc function - * @name ng.sce#isEnabled - * @methodOf ng.$sce - * @function + * @ngdoc method + * @name $sce#isEnabled + * @kind function * - * @return {Boolean} true if SCE is enabled, false otherwise. If you want to set the value, you - * have to do it at module config time on {@link ng.$sceProvider $sceProvider}. + * @return {Boolean} True if SCE is enabled, false otherwise. If you want to set the value, you + * have to do it at module config time on {@link ng.$sceProvider $sceProvider}. * * @description * Returns a boolean indicating if SCE is enabled. */ - sce.isEnabled = function () { + sce.isEnabled = function() { return enabled; }; sce.trustAs = $sceDelegate.trustAs; @@ -13135,307 +19767,300 @@ /** * @ngdoc method - * @name ng.$sce#parse - * @methodOf ng.$sce + * @name $sce#parseAs * * @description * Converts Angular {@link guide/expression expression} into a function. This is like {@link - * ng.$parse $parse} and is identical when the expression is a literal constant. Otherwise, it - * wraps the expression in a call to {@link ng.$sce#methods_getTrusted $sce.getTrusted(*type*, - * *result*)} + * ng.$parse $parse} and is identical when the expression is a literal constant. Otherwise, it + * wraps the expression in a call to {@link ng.$sce#getTrusted $sce.getTrusted(*type*, + * *result*)} * - * @param {string} type The kind of SCE context in which this result will be used. + * @param {string} type The SCE context in which this result will be used. * @param {string} expression String expression to compile. - * @returns {function(context, locals)} a function which represents the compiled expression: + * @return {function(context, locals)} A function which represents the compiled expression: * - * * `context` – `{object}` – an object against which any expressions embedded in the strings - * are evaluated against (typically a scope object). - * * `locals` – `{object=}` – local variables context object, useful for overriding values in - * `context`. + * * `context` – `{object}` – an object against which any expressions embedded in the + * strings are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values + * in `context`. */ sce.parseAs = function sceParseAs(type, expr) { var parsed = $parse(expr); if (parsed.literal && parsed.constant) { return parsed; } else { - return function sceParseAsTrusted(self, locals) { - return sce.getTrusted(type, parsed(self, locals)); - }; + return $parse(expr, function(value) { + return sce.getTrusted(type, value); + }); } }; /** * @ngdoc method - * @name ng.$sce#trustAs - * @methodOf ng.$sce + * @name $sce#trustAs * * @description - * Delegates to {@link ng.$sceDelegate#methods_trustAs `$sceDelegate.trustAs`}. As such, - * returns an objectthat is trusted by angular for use in specified strict contextual - * escaping contexts (such as ng-html-bind-unsafe, ng-include, any src attribute - * interpolation, any dom event binding attribute interpolation such as for onclick, etc.) - * that uses the provided value. See * {@link ng.$sce $sce} for enabling strict contextual - * escaping. + * Delegates to {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}. As such, returns a + * wrapped object that represents your value, and the trust you have in its safety for the given + * context. AngularJS can then use that value as-is in bindings of the specified secure context. + * This is used in bindings for `ng-bind-html`, `ng-include`, and most `src` attribute + * interpolations. See {@link ng.$sce $sce} for strict contextual escaping. * - * @param {string} type The kind of context in which this value is safe for use. e.g. url, - * resource_url, html, js and css. - * @param {*} value The value that that should be considered trusted/safe. - * @returns {*} A value that can be used to stand in for the provided `value` in places - * where Angular expects a $sce.trustAs() return value. + * @param {string} type The context in which this value is safe for use, e.g. `$sce.URL`, + * `$sce.RESOURCE_URL`, `$sce.HTML`, `$sce.JS` or `$sce.CSS`. + * + * @param {*} value The value that that should be considered trusted. + * @return {*} A wrapped version of value that can be used as a trusted variant of your `value` + * in the context you specified. */ /** * @ngdoc method - * @name ng.$sce#trustAsHtml - * @methodOf ng.$sce + * @name $sce#trustAsHtml * * @description * Shorthand method. `$sce.trustAsHtml(value)` → - * {@link ng.$sceDelegate#methods_trustAs `$sceDelegate.trustAs($sce.HTML, value)`} + * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.HTML, value)`} * - * @param {*} value The value to trustAs. - * @returns {*} An object that can be passed to {@link ng.$sce#methods_getTrustedHtml - * $sce.getTrustedHtml(value)} to obtain the original value. (privileged directives - * only accept expressions that are either literal constants or are the - * return value of {@link ng.$sce#methods_trustAs $sce.trustAs}.) + * @param {*} value The value to mark as trusted for `$sce.HTML` context. + * @return {*} A wrapped version of value that can be used as a trusted variant of your `value` + * in `$sce.HTML` context (like `ng-bind-html`). */ /** * @ngdoc method - * @name ng.$sce#trustAsUrl - * @methodOf ng.$sce + * @name $sce#trustAsCss + * + * @description + * Shorthand method. `$sce.trustAsCss(value)` → + * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.CSS, value)`} + * + * @param {*} value The value to mark as trusted for `$sce.CSS` context. + * @return {*} A wrapped version of value that can be used as a trusted variant + * of your `value` in `$sce.CSS` context. This context is currently unused, so there are + * almost no reasons to use this function so far. + */ + + /** + * @ngdoc method + * @name $sce#trustAsUrl * * @description * Shorthand method. `$sce.trustAsUrl(value)` → - * {@link ng.$sceDelegate#methods_trustAs `$sceDelegate.trustAs($sce.URL, value)`} + * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.URL, value)`} * - * @param {*} value The value to trustAs. - * @returns {*} An object that can be passed to {@link ng.$sce#methods_getTrustedUrl - * $sce.getTrustedUrl(value)} to obtain the original value. (privileged directives - * only accept expressions that are either literal constants or are the - * return value of {@link ng.$sce#methods_trustAs $sce.trustAs}.) + * @param {*} value The value to mark as trusted for `$sce.URL` context. + * @return {*} A wrapped version of value that can be used as a trusted variant of your `value` + * in `$sce.URL` context. That context is currently unused, so there are almost no reasons + * to use this function so far. */ /** * @ngdoc method - * @name ng.$sce#trustAsResourceUrl - * @methodOf ng.$sce + * @name $sce#trustAsResourceUrl * * @description * Shorthand method. `$sce.trustAsResourceUrl(value)` → - * {@link ng.$sceDelegate#methods_trustAs `$sceDelegate.trustAs($sce.RESOURCE_URL, value)`} + * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.RESOURCE_URL, value)`} * - * @param {*} value The value to trustAs. - * @returns {*} An object that can be passed to {@link ng.$sce#methods_getTrustedResourceUrl - * $sce.getTrustedResourceUrl(value)} to obtain the original value. (privileged directives - * only accept expressions that are either literal constants or are the return - * value of {@link ng.$sce#methods_trustAs $sce.trustAs}.) + * @param {*} value The value to mark as trusted for `$sce.RESOURCE_URL` context. + * @return {*} A wrapped version of value that can be used as a trusted variant of your `value` + * in `$sce.RESOURCE_URL` context (template URLs in `ng-include`, most `src` attribute + * bindings, ...) */ /** * @ngdoc method - * @name ng.$sce#trustAsJs - * @methodOf ng.$sce + * @name $sce#trustAsJs * * @description * Shorthand method. `$sce.trustAsJs(value)` → - * {@link ng.$sceDelegate#methods_trustAs `$sceDelegate.trustAs($sce.JS, value)`} + * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.JS, value)`} * - * @param {*} value The value to trustAs. - * @returns {*} An object that can be passed to {@link ng.$sce#methods_getTrustedJs - * $sce.getTrustedJs(value)} to obtain the original value. (privileged directives - * only accept expressions that are either literal constants or are the - * return value of {@link ng.$sce#methods_trustAs $sce.trustAs}.) + * @param {*} value The value to mark as trusted for `$sce.JS` context. + * @return {*} A wrapped version of value that can be used as a trusted variant of your `value` + * in `$sce.JS` context. That context is currently unused, so there are almost no reasons to + * use this function so far. */ /** * @ngdoc method - * @name ng.$sce#getTrusted - * @methodOf ng.$sce + * @name $sce#getTrusted * * @description - * Delegates to {@link ng.$sceDelegate#methods_getTrusted `$sceDelegate.getTrusted`}. As such, - * takes the result of a {@link ng.$sce#methods_trustAs `$sce.trustAs`}() call and returns the - * originally supplied value if the queried context type is a supertype of the created type. - * If this condition isn't satisfied, throws an exception. + * Delegates to {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted`}. As such, + * takes any input, and either returns a value that's safe to use in the specified context, + * or throws an exception. This function is aware of trusted values created by the `trustAs` + * function and its shorthands, and when contexts are appropriate, returns the unwrapped value + * as-is. Finally, this function can also throw when there is no way to turn `maybeTrusted` in a + * safe value (e.g., no sanitization is available or possible.) * - * @param {string} type The kind of context in which this value is to be used. - * @param {*} maybeTrusted The result of a prior {@link ng.$sce#methods_trustAs `$sce.trustAs`} - * call. - * @returns {*} The value the was originally provided to - * {@link ng.$sce#methods_trustAs `$sce.trustAs`} if valid in this context. - * Otherwise, throws an exception. + * @param {string} type The context in which this value is to be used. + * @param {*} maybeTrusted The result of a prior {@link ng.$sce#trustAs + * `$sce.trustAs`} call, or anything else (which will not be considered trusted.) + * @return {*} A version of the value that's safe to use in the given context, or throws an + * exception if this is impossible. */ /** * @ngdoc method - * @name ng.$sce#getTrustedHtml - * @methodOf ng.$sce + * @name $sce#getTrustedHtml * * @description * Shorthand method. `$sce.getTrustedHtml(value)` → - * {@link ng.$sceDelegate#methods_getTrusted `$sceDelegate.getTrusted($sce.HTML, value)`} + * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.HTML, value)`} * * @param {*} value The value to pass to `$sce.getTrusted`. - * @returns {*} The return value of `$sce.getTrusted($sce.HTML, value)` + * @return {*} The return value of `$sce.getTrusted($sce.HTML, value)` */ /** * @ngdoc method - * @name ng.$sce#getTrustedCss - * @methodOf ng.$sce + * @name $sce#getTrustedCss * * @description * Shorthand method. `$sce.getTrustedCss(value)` → - * {@link ng.$sceDelegate#methods_getTrusted `$sceDelegate.getTrusted($sce.CSS, value)`} + * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.CSS, value)`} * * @param {*} value The value to pass to `$sce.getTrusted`. - * @returns {*} The return value of `$sce.getTrusted($sce.CSS, value)` + * @return {*} The return value of `$sce.getTrusted($sce.CSS, value)` */ /** * @ngdoc method - * @name ng.$sce#getTrustedUrl - * @methodOf ng.$sce + * @name $sce#getTrustedUrl * * @description * Shorthand method. `$sce.getTrustedUrl(value)` → - * {@link ng.$sceDelegate#methods_getTrusted `$sceDelegate.getTrusted($sce.URL, value)`} + * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.URL, value)`} * * @param {*} value The value to pass to `$sce.getTrusted`. - * @returns {*} The return value of `$sce.getTrusted($sce.URL, value)` + * @return {*} The return value of `$sce.getTrusted($sce.URL, value)` */ /** * @ngdoc method - * @name ng.$sce#getTrustedResourceUrl - * @methodOf ng.$sce + * @name $sce#getTrustedResourceUrl * * @description * Shorthand method. `$sce.getTrustedResourceUrl(value)` → - * {@link ng.$sceDelegate#methods_getTrusted `$sceDelegate.getTrusted($sce.RESOURCE_URL, value)`} + * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.RESOURCE_URL, value)`} * * @param {*} value The value to pass to `$sceDelegate.getTrusted`. - * @returns {*} The return value of `$sce.getTrusted($sce.RESOURCE_URL, value)` + * @return {*} The return value of `$sce.getTrusted($sce.RESOURCE_URL, value)` */ /** * @ngdoc method - * @name ng.$sce#getTrustedJs - * @methodOf ng.$sce + * @name $sce#getTrustedJs * * @description * Shorthand method. `$sce.getTrustedJs(value)` → - * {@link ng.$sceDelegate#methods_getTrusted `$sceDelegate.getTrusted($sce.JS, value)`} + * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.JS, value)`} * * @param {*} value The value to pass to `$sce.getTrusted`. - * @returns {*} The return value of `$sce.getTrusted($sce.JS, value)` + * @return {*} The return value of `$sce.getTrusted($sce.JS, value)` */ /** * @ngdoc method - * @name ng.$sce#parseAsHtml - * @methodOf ng.$sce + * @name $sce#parseAsHtml * * @description * Shorthand method. `$sce.parseAsHtml(expression string)` → - * {@link ng.$sce#methods_parse `$sce.parseAs($sce.HTML, value)`} + * {@link ng.$sce#parseAs `$sce.parseAs($sce.HTML, value)`} * * @param {string} expression String expression to compile. - * @returns {function(context, locals)} a function which represents the compiled expression: + * @return {function(context, locals)} A function which represents the compiled expression: * - * * `context` – `{object}` – an object against which any expressions embedded in the strings - * are evaluated against (typically a scope object). - * * `locals` – `{object=}` – local variables context object, useful for overriding values in - * `context`. + * * `context` – `{object}` – an object against which any expressions embedded in the + * strings are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values + * in `context`. */ /** * @ngdoc method - * @name ng.$sce#parseAsCss - * @methodOf ng.$sce + * @name $sce#parseAsCss * * @description * Shorthand method. `$sce.parseAsCss(value)` → - * {@link ng.$sce#methods_parse `$sce.parseAs($sce.CSS, value)`} + * {@link ng.$sce#parseAs `$sce.parseAs($sce.CSS, value)`} * * @param {string} expression String expression to compile. - * @returns {function(context, locals)} a function which represents the compiled expression: + * @return {function(context, locals)} A function which represents the compiled expression: * - * * `context` – `{object}` – an object against which any expressions embedded in the strings - * are evaluated against (typically a scope object). - * * `locals` – `{object=}` – local variables context object, useful for overriding values in - * `context`. + * * `context` – `{object}` – an object against which any expressions embedded in the + * strings are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values + * in `context`. */ /** * @ngdoc method - * @name ng.$sce#parseAsUrl - * @methodOf ng.$sce + * @name $sce#parseAsUrl * * @description * Shorthand method. `$sce.parseAsUrl(value)` → - * {@link ng.$sce#methods_parse `$sce.parseAs($sce.URL, value)`} + * {@link ng.$sce#parseAs `$sce.parseAs($sce.URL, value)`} * * @param {string} expression String expression to compile. - * @returns {function(context, locals)} a function which represents the compiled expression: + * @return {function(context, locals)} A function which represents the compiled expression: * - * * `context` – `{object}` – an object against which any expressions embedded in the strings - * are evaluated against (typically a scope object). - * * `locals` – `{object=}` – local variables context object, useful for overriding values in - * `context`. + * * `context` – `{object}` – an object against which any expressions embedded in the + * strings are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values + * in `context`. */ /** * @ngdoc method - * @name ng.$sce#parseAsResourceUrl - * @methodOf ng.$sce + * @name $sce#parseAsResourceUrl * * @description * Shorthand method. `$sce.parseAsResourceUrl(value)` → - * {@link ng.$sce#methods_parse `$sce.parseAs($sce.RESOURCE_URL, value)`} + * {@link ng.$sce#parseAs `$sce.parseAs($sce.RESOURCE_URL, value)`} * * @param {string} expression String expression to compile. - * @returns {function(context, locals)} a function which represents the compiled expression: + * @return {function(context, locals)} A function which represents the compiled expression: * - * * `context` – `{object}` – an object against which any expressions embedded in the strings - * are evaluated against (typically a scope object). - * * `locals` – `{object=}` – local variables context object, useful for overriding values in - * `context`. + * * `context` – `{object}` – an object against which any expressions embedded in the + * strings are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values + * in `context`. */ /** * @ngdoc method - * @name ng.$sce#parseAsJs - * @methodOf ng.$sce + * @name $sce#parseAsJs * * @description * Shorthand method. `$sce.parseAsJs(value)` → - * {@link ng.$sce#methods_parse `$sce.parseAs($sce.JS, value)`} + * {@link ng.$sce#parseAs `$sce.parseAs($sce.JS, value)`} * * @param {string} expression String expression to compile. - * @returns {function(context, locals)} a function which represents the compiled expression: + * @return {function(context, locals)} A function which represents the compiled expression: * - * * `context` – `{object}` – an object against which any expressions embedded in the strings - * are evaluated against (typically a scope object). - * * `locals` – `{object=}` – local variables context object, useful for overriding values in - * `context`. + * * `context` – `{object}` – an object against which any expressions embedded in the + * strings are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values + * in `context`. */ - // Shorthand delegations. + // Shorthand delegations. var parse = sce.parseAs, - getTrusted = sce.getTrusted, - trustAs = sce.trustAs; + getTrusted = sce.getTrusted, + trustAs = sce.trustAs; - forEach(SCE_CONTEXTS, function (enumValue, name) { + forEach(SCE_CONTEXTS, function(enumValue, name) { var lName = lowercase(name); - sce[camelCase("parse_as_" + lName)] = function (expr) { + sce[snakeToCamel('parse_as_' + lName)] = function(expr) { return parse(enumValue, expr); }; - sce[camelCase("get_trusted_" + lName)] = function (value) { + sce[snakeToCamel('get_trusted_' + lName)] = function(value) { return getTrusted(enumValue, value); }; - sce[camelCase("trust_as_" + lName)] = function (value) { + sce[snakeToCamel('trust_as_' + lName)] = function(value) { return trustAs(enumValue, value); }; }); @@ -13444,15 +20069,17 @@ }]; } + /* exported $SnifferProvider */ + /** * !!! This is an undocumented "private" service !!! * - * @name ng.$sniffer + * @name $sniffer * @requires $window * @requires $document + * @this * * @property {boolean} history Does the browser support html5 history api ? - * @property {boolean} hashchange Does the browser support hashchange event ? * @property {boolean} transitions Does the browser support CSS transition events ? * @property {boolean} animations Does the browser support CSS animation events ? * @@ -13462,38 +20089,32 @@ function $SnifferProvider() { this.$get = ['$window', '$document', function($window, $document) { var eventSupport = {}, - android = - int((/android (\d+)/.exec(lowercase(($window.navigator || {}).userAgent)) || [])[1]), - boxee = /Boxee/i.test(($window.navigator || {}).userAgent), - document = $document[0] || {}, - documentMode = document.documentMode, - vendorPrefix, - vendorRegex = /^(Moz|webkit|O|ms)(?=[A-Z])/, - bodyStyle = document.body && document.body.style, - transitions = false, - animations = false, - match; + // Chrome Packaged Apps are not allowed to access `history.pushState`. + // If not sandboxed, they can be detected by the presence of `chrome.app.runtime` + // (see https://developer.chrome.com/apps/api_index). If sandboxed, they can be detected by + // the presence of an extension runtime ID and the absence of other Chrome runtime APIs + // (see https://developer.chrome.com/apps/manifest/sandbox). + // (NW.js apps have access to Chrome APIs, but do support `history`.) + isNw = $window.nw && $window.nw.process, + isChromePackagedApp = + !isNw && + $window.chrome && + ($window.chrome.app && $window.chrome.app.runtime || + !$window.chrome.app && $window.chrome.runtime && $window.chrome.runtime.id), + hasHistoryPushState = !isChromePackagedApp && $window.history && $window.history.pushState, + android = + toInt((/android (\d+)/.exec(lowercase(($window.navigator || {}).userAgent)) || [])[1]), + boxee = /Boxee/i.test(($window.navigator || {}).userAgent), + document = $document[0] || {}, + bodyStyle = document.body && document.body.style, + transitions = false, + animations = false; if (bodyStyle) { - for(var prop in bodyStyle) { - if(match = vendorRegex.exec(prop)) { - vendorPrefix = match[0]; - vendorPrefix = vendorPrefix.substr(0, 1).toUpperCase() + vendorPrefix.substr(1); - break; - } - } - - if(!vendorPrefix) { - vendorPrefix = ('WebkitOpacity' in bodyStyle) && 'webkit'; - } - - transitions = !!(('transition' in bodyStyle) || (vendorPrefix + 'Transition' in bodyStyle)); - animations = !!(('animation' in bodyStyle) || (vendorPrefix + 'Animation' in bodyStyle)); - - if (android && (!transitions||!animations)) { - transitions = isString(document.body.style.webkitTransition); - animations = isString(document.body.style.webkitAnimation); - } + // Support: Android <5, Blackberry Browser 10, default Chrome in Android 4.4.x + // Mentioned browsers need a -webkit- prefix for transitions & animations. + transitions = !!('transition' in bodyStyle || 'webkitTransition' in bodyStyle); + animations = !!('animation' in bodyStyle || 'webkitAnimation' in bodyStyle); } @@ -13506,17 +20127,15 @@ // older webkit browser (533.9) on Boxee box has exactly the same problem as Android has // so let's not use the history API also // We are purposefully using `!(android < 4)` to cover the case when `android` is undefined - // jshint -W018 - history: !!($window.history && $window.history.pushState && !(android < 4) && !boxee), - // jshint +W018 - hashchange: 'onhashchange' in $window && - // IE8 compatible mode lies - (!documentMode || documentMode > 7), + history: !!(hasHistoryPushState && !(android < 4) && !boxee), hasEvent: function(event) { + // Support: IE 9-11 only // IE9 implements 'input' event it's so fubared that we rather pretend that it doesn't have // it. In particular the event is not fired when backspace or delete key are pressed or // when cut operation is performed. - if (event == 'input' && msie == 9) return false; + // IE10+ implements 'input' event but it erroneously fires under various situations, + // e.g. when placeholder changes, or a form is focused. + if (event === 'input' && msie) return false; if (isUndefined(eventSupport[event])) { var divElm = document.createElement('div'); @@ -13526,62 +20145,303 @@ return eventSupport[event]; }, csp: csp(), - vendorPrefix: vendorPrefix, - transitions : transitions, - animations : animations, - android: android, - msie : msie, - msieDocumentMode: documentMode + transitions: transitions, + animations: animations, + android: android }; }]; } + var $templateRequestMinErr = minErr('$compile'); + + /** + * @ngdoc provider + * @name $templateRequestProvider + * @this + * + * @description + * Used to configure the options passed to the {@link $http} service when making a template request. + * + * For example, it can be used for specifying the "Accept" header that is sent to the server, when + * requesting a template. + */ + function $TemplateRequestProvider() { + + var httpOptions; + + /** + * @ngdoc method + * @name $templateRequestProvider#httpOptions + * @description + * The options to be passed to the {@link $http} service when making the request. + * You can use this to override options such as the "Accept" header for template requests. + * + * The {@link $templateRequest} will set the `cache` and the `transformResponse` properties of the + * options if not overridden here. + * + * @param {string=} value new value for the {@link $http} options. + * @returns {string|self} Returns the {@link $http} options when used as getter and self if used as setter. + */ + this.httpOptions = function(val) { + if (val) { + httpOptions = val; + return this; + } + return httpOptions; + }; + + /** + * @ngdoc service + * @name $templateRequest + * + * @description + * The `$templateRequest` service runs security checks then downloads the provided template using + * `$http` and, upon success, stores the contents inside of `$templateCache`. If the HTTP request + * fails or the response data of the HTTP request is empty, a `$compile` error will be thrown (the + * exception can be thwarted by setting the 2nd parameter of the function to true). Note that the + * contents of `$templateCache` are trusted, so the call to `$sce.getTrustedUrl(tpl)` is omitted + * when `tpl` is of type string and `$templateCache` has the matching entry. + * + * If you want to pass custom options to the `$http` service, such as setting the Accept header you + * can configure this via {@link $templateRequestProvider#httpOptions}. + * + * @param {string|TrustedResourceUrl} tpl The HTTP request template URL + * @param {boolean=} ignoreRequestError Whether or not to ignore the exception when the request fails or the template is empty + * + * @return {Promise} a promise for the HTTP response data of the given URL. + * + * @property {number} totalPendingRequests total amount of pending template requests being downloaded. + */ + this.$get = ['$exceptionHandler', '$templateCache', '$http', '$q', '$sce', + function($exceptionHandler, $templateCache, $http, $q, $sce) { + + function handleRequestFn(tpl, ignoreRequestError) { + handleRequestFn.totalPendingRequests++; + + // We consider the template cache holds only trusted templates, so + // there's no need to go through whitelisting again for keys that already + // are included in there. This also makes Angular accept any script + // directive, no matter its name. However, we still need to unwrap trusted + // types. + if (!isString(tpl) || isUndefined($templateCache.get(tpl))) { + tpl = $sce.getTrustedResourceUrl(tpl); + } + + var transformResponse = $http.defaults && $http.defaults.transformResponse; + + if (isArray(transformResponse)) { + transformResponse = transformResponse.filter(function(transformer) { + return transformer !== defaultHttpResponseTransform; + }); + } else if (transformResponse === defaultHttpResponseTransform) { + transformResponse = null; + } + + return $http.get(tpl, extend({ + cache: $templateCache, + transformResponse: transformResponse + }, httpOptions)) + .finally(function() { + handleRequestFn.totalPendingRequests--; + }) + .then(function(response) { + $templateCache.put(tpl, response.data); + return response.data; + }, handleError); + + function handleError(resp) { + if (!ignoreRequestError) { + resp = $templateRequestMinErr('tpload', + 'Failed to load template: {0} (HTTP status: {1} {2})', + tpl, resp.status, resp.statusText); + + $exceptionHandler(resp); + } + + return $q.reject(resp); + } + } + + handleRequestFn.totalPendingRequests = 0; + + return handleRequestFn; + } + ]; + } + + /** @this */ + function $$TestabilityProvider() { + this.$get = ['$rootScope', '$browser', '$location', + function($rootScope, $browser, $location) { + + /** + * @name $testability + * + * @description + * The private $$testability service provides a collection of methods for use when debugging + * or by automated test and debugging tools. + */ + var testability = {}; + + /** + * @name $$testability#findBindings + * + * @description + * Returns an array of elements that are bound (via ng-bind or {{}}) + * to expressions matching the input. + * + * @param {Element} element The element root to search from. + * @param {string} expression The binding expression to match. + * @param {boolean} opt_exactMatch If true, only returns exact matches + * for the expression. Filters and whitespace are ignored. + */ + testability.findBindings = function(element, expression, opt_exactMatch) { + var bindings = element.getElementsByClassName('ng-binding'); + var matches = []; + forEach(bindings, function(binding) { + var dataBinding = angular.element(binding).data('$binding'); + if (dataBinding) { + forEach(dataBinding, function(bindingName) { + if (opt_exactMatch) { + var matcher = new RegExp('(^|\\s)' + escapeForRegexp(expression) + '(\\s|\\||$)'); + if (matcher.test(bindingName)) { + matches.push(binding); + } + } else { + if (bindingName.indexOf(expression) !== -1) { + matches.push(binding); + } + } + }); + } + }); + return matches; + }; + + /** + * @name $$testability#findModels + * + * @description + * Returns an array of elements that are two-way found via ng-model to + * expressions matching the input. + * + * @param {Element} element The element root to search from. + * @param {string} expression The model expression to match. + * @param {boolean} opt_exactMatch If true, only returns exact matches + * for the expression. + */ + testability.findModels = function(element, expression, opt_exactMatch) { + var prefixes = ['ng-', 'data-ng-', 'ng\\:']; + for (var p = 0; p < prefixes.length; ++p) { + var attributeEquals = opt_exactMatch ? '=' : '*='; + var selector = '[' + prefixes[p] + 'model' + attributeEquals + '"' + expression + '"]'; + var elements = element.querySelectorAll(selector); + if (elements.length) { + return elements; + } + } + }; + + /** + * @name $$testability#getLocation + * + * @description + * Shortcut for getting the location in a browser agnostic way. Returns + * the path, search, and hash. (e.g. /path?a=b#hash) + */ + testability.getLocation = function() { + return $location.url(); + }; + + /** + * @name $$testability#setLocation + * + * @description + * Shortcut for navigating to a location without doing a full page reload. + * + * @param {string} url The location url (path, search and hash, + * e.g. /path?a=b#hash) to go to. + */ + testability.setLocation = function(url) { + if (url !== $location.url()) { + $location.url(url); + $rootScope.$digest(); + } + }; + + /** + * @name $$testability#whenStable + * + * @description + * Calls the callback when $timeout and $http requests are completed. + * + * @param {function} callback + */ + testability.whenStable = function(callback) { + $browser.notifyWhenNoOutstandingRequests(callback); + }; + + return testability; + }]; + } + + /** @this */ function $TimeoutProvider() { - this.$get = ['$rootScope', '$browser', '$q', '$exceptionHandler', - function($rootScope, $browser, $q, $exceptionHandler) { + this.$get = ['$rootScope', '$browser', '$q', '$$q', '$exceptionHandler', + function($rootScope, $browser, $q, $$q, $exceptionHandler) { + var deferreds = {}; /** - * @ngdoc function - * @name ng.$timeout - * @requires $browser + * @ngdoc service + * @name $timeout * * @description * Angular's wrapper for `window.setTimeout`. The `fn` function is wrapped into a try/catch * block and delegates any exceptions to * {@link ng.$exceptionHandler $exceptionHandler} service. * - * The return value of registering a timeout function is a promise, which will be resolved when - * the timeout is reached and the timeout function is executed. + * The return value of calling `$timeout` is a promise, which will be resolved when + * the delay has passed and the timeout function, if provided, is executed. * * To cancel a timeout request, call `$timeout.cancel(promise)`. * * In tests you can use {@link ngMock.$timeout `$timeout.flush()`} to * synchronously flush the queue of deferred functions. * - * @param {function()} fn A function, whose execution should be delayed. + * If you only want a promise that will be resolved after some specified delay + * then you can call `$timeout` without the `fn` function. + * + * @param {function()=} fn A function, whose execution should be delayed. * @param {number=} [delay=0] Delay in milliseconds. * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise - * will invoke `fn` within the {@link ng.$rootScope.Scope#methods_$apply $apply} block. - * @returns {Promise} Promise that will be resolved when the timeout is reached. The value this - * promise will be resolved with is the return value of the `fn` function. + * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block. + * @param {...*=} Pass additional parameters to the executed function. + * @returns {Promise} Promise that will be resolved when the timeout is reached. The promise + * will be resolved with the return value of the `fn` function. * */ function timeout(fn, delay, invokeApply) { - var deferred = $q.defer(), - promise = deferred.promise, - skipApply = (isDefined(invokeApply) && !invokeApply), - timeoutId; + if (!isFunction(fn)) { + invokeApply = delay; + delay = fn; + fn = noop; + } + + var args = sliceArgs(arguments, 3), + skipApply = (isDefined(invokeApply) && !invokeApply), + deferred = (skipApply ? $$q : $q).defer(), + promise = deferred.promise, + timeoutId; timeoutId = $browser.defer(function() { try { - deferred.resolve(fn()); - } catch(e) { + deferred.resolve(fn.apply(null, args)); + } catch (e) { deferred.reject(e); $exceptionHandler(e); - } - finally { + } finally { delete deferreds[promise.$$timeoutId]; } @@ -13596,9 +20456,8 @@ /** - * @ngdoc function - * @name ng.$timeout#cancel - * @methodOf ng.$timeout + * @ngdoc method + * @name $timeout#cancel * * @description * Cancels a task associated with the `promise`. As a result of this, the promise will be @@ -13610,6 +20469,8 @@ */ timeout.cancel = function(promise) { if (promise && promise.$$timeoutId in deferreds) { + // Timeout cancels should not report an unhandled promise. + markQExceptionHandled(deferreds[promise.$$timeoutId].promise); deferreds[promise.$$timeoutId].reject('canceled'); delete deferreds[promise.$$timeoutId]; return $browser.defer.cancel(promise.$$timeoutId); @@ -13628,8 +20489,8 @@ // doesn't know about mocked locations and resolves URLs to the real document - which is // exactly the behavior needed here. There is little value is mocking these out for this // service. - var urlParsingNode = document.createElement("a"); - var originUrl = urlResolve(window.location.href, true); + var urlParsingNode = window.document.createElement('a'); + var originUrl = urlResolve(window.location.href); /** @@ -13641,33 +20502,26 @@ * URL will be resolved into an absolute URL in the context of the application document. * Parsing means that the anchor node's host, hostname, protocol, port, pathname and related * properties are all populated to reflect the normalized URL. This approach has wide - * compatibility - Safari 1+, Mozilla 1+, Opera 7+,e etc. See - * http://www.aptana.com/reference/html/../api/HTMLAnchorElement.html + * compatibility - Safari 1+, Mozilla 1+ etc. See + * http://www.aptana.com/reference/html/api/HTMLAnchorElement.html * * Implementation Notes for IE * --------------------------- - * IE >= 8 and <= 10 normalizes the URL when assigned to the anchor node similar to the other + * IE <= 10 normalizes the URL when assigned to the anchor node similar to the other * browsers. However, the parsed components will not be set if the URL assigned did not specify * them. (e.g. if you assign a.href = "foo", then a.protocol, a.host, etc. will be empty.) We * work around that by performing the parsing in a 2nd step by taking a previously normalized * URL (e.g. by assigning to a.href) and assigning it a.href again. This correctly populates the * properties such as protocol, hostname, port, etc. * - * IE7 does not normalize the URL when assigned to an anchor node. (Apparently, it does, if one - * uses the inner HTML approach to assign the URL as part of an HTML snippet - - * http://stackoverflow.com/a/472729) However, setting img[src] does normalize the URL. - * Unfortunately, setting img[src] to something like "javascript:foo" on IE throws an exception. - * Since the primary usage for normalizing URLs is to sanitize such URLs, we can't use that - * method and IE < 8 is unsupported. - * * References: * http://developer.mozilla.org/en-US/docs/Web/API/HTMLAnchorElement - * http://www.aptana.com/reference/html/../api/HTMLAnchorElement.html + * http://www.aptana.com/reference/html/api/HTMLAnchorElement.html * http://url.spec.whatwg.org/#urlutils * https://github.com/angular/angular.js/pull/2902 * http://james.padolsey.com/javascript/parsing-urls-with-the-dom/ * - * @function + * @kind function * @param {string} url The URL to be parsed. * @description Normalizes and parses a URL. * @returns {object} Returns the normalized URL as a dictionary. @@ -13684,13 +20538,14 @@ * | pathname | The pathname, beginning with "/" * */ - function urlResolve(url, base) { + function urlResolve(url) { var href = url; + // Support: IE 9-11 only if (msie) { // Normalize before parse. Refer Implementation Notes on why this is // done in two steps on IE. - urlParsingNode.setAttribute("href", href); + urlParsingNode.setAttribute('href', href); href = urlParsingNode.href; } @@ -13706,8 +20561,8 @@ hostname: urlParsingNode.hostname, port: urlParsingNode.port, pathname: (urlParsingNode.pathname.charAt(0) === '/') - ? urlParsingNode.pathname - : '/' + urlParsingNode.pathname + ? urlParsingNode.pathname + : '/' + urlParsingNode.pathname }; } @@ -13721,12 +20576,13 @@ function urlIsSameOrigin(requestUrl) { var parsed = (isString(requestUrl)) ? urlResolve(requestUrl) : requestUrl; return (parsed.protocol === originUrl.protocol && - parsed.host === originUrl.host); + parsed.host === originUrl.host); } /** - * @ngdoc object - * @name ng.$window + * @ngdoc service + * @name $window + * @this * * @description * A reference to the browser's `window` object. While `window` @@ -13740,44 +20596,127 @@ * expression. * * @example - - + + -
    - +
    +
    - - + + it('should display the greeting in the input box', function() { - input('greeting').enter('Hello, E2E Tests'); + element(by.model('greeting')).sendKeys('Hello, E2E Tests'); // If we click the button it will block the test runner // element(':button').click(); }); - - + + */ - function $WindowProvider(){ + function $WindowProvider() { this.$get = valueFn(window); } /** - * @ngdoc object - * @name ng.$filterProvider + * @name $$cookieReader + * @requires $document + * + * @description + * This is a private service for reading cookies used by $http and ngCookies + * + * @return {Object} a key/value map of the current cookies + */ + function $$CookieReader($document) { + var rawDocument = $document[0] || {}; + var lastCookies = {}; + var lastCookieString = ''; + + function safeGetCookie(rawDocument) { + try { + return rawDocument.cookie || ''; + } catch (e) { + return ''; + } + } + + function safeDecodeURIComponent(str) { + try { + return decodeURIComponent(str); + } catch (e) { + return str; + } + } + + return function() { + var cookieArray, cookie, i, index, name; + var currentCookieString = safeGetCookie(rawDocument); + + if (currentCookieString !== lastCookieString) { + lastCookieString = currentCookieString; + cookieArray = lastCookieString.split('; '); + lastCookies = {}; + + for (i = 0; i < cookieArray.length; i++) { + cookie = cookieArray[i]; + index = cookie.indexOf('='); + if (index > 0) { //ignore nameless cookies + name = safeDecodeURIComponent(cookie.substring(0, index)); + // the first value that is seen for a cookie is the most + // specific one. values for the same cookie name that + // follow are for less specific paths. + if (isUndefined(lastCookies[name])) { + lastCookies[name] = safeDecodeURIComponent(cookie.substring(index + 1)); + } + } + } + } + return lastCookies; + }; + } + + $$CookieReader.$inject = ['$document']; + + /** @this */ + function $$CookieReaderProvider() { + this.$get = $$CookieReader; + } + + /* global currencyFilter: true, + dateFilter: true, + filterFilter: true, + jsonFilter: true, + limitToFilter: true, + lowercaseFilter: true, + numberFilter: true, + orderByFilter: true, + uppercaseFilter: true, + */ + + /** + * @ngdoc provider + * @name $filterProvider * @description * * Filters are just functions which transform input to an output. However filters need to be * Dependency Injected. To achieve this a filter definition consists of a factory function which is * annotated with dependencies and is responsible for creating a filter function. * - *
    +   * 
    + * **Note:** Filter names must be valid angular {@link expression} identifiers, such as `uppercase` or `orderBy`. + * Names with special characters, such as hyphens and dots, are not allowed. If you wish to namespace + * your filters, then you can use capitalization (`myappSubsectionFilterx`) or underscores + * (`myapp_subsection_filterx`). + *
    + * + * ```js * // Filter registration * function MyModule($provide, $filterProvider) { * // create a service to demonstrate injection (not always needed) @@ -13796,12 +20735,12 @@ * }; * }); * } - *
    + * ``` * * The filter function is registered with the `$injector` under the filter name suffix with * `Filter`. * - *
    +   * ```js
        *   it('should be the same instance', inject(
        *     function($filterProvider) {
      *       $filterProvider.register('reverse', function(){
    @@ -13811,53 +20750,73 @@
        *     function($filter, reverseFilter) {
      *       expect($filter('reverse')).toBe(reverseFilter);
      *     });
    -   * 
    + * ``` * * * For more information about how angular filters work, and how to create your own filters, see * {@link guide/filter Filters} in the Angular Developer Guide. */ - /** - * @ngdoc method - * @name ng.$filterProvider#register - * @methodOf ng.$filterProvider - * @description - * Register filter factory function. - * - * @param {String} name Name of the filter. - * @param {function} fn The filter factory function which is injectable. - */ - /** - * @ngdoc function - * @name ng.$filter - * @function + * @ngdoc service + * @name $filter + * @kind function * @description * Filters are used for formatting data displayed to the user. * + * They can be used in view templates, controllers or services.Angular comes + * with a collection of [built-in filters](api/ng/filter), but it is easy to + * define your own as well. + * * The general syntax in templates is as follows: * - * {{ expression [| filter_name[:parameter_value] ... ] }} + * ```html + * {{ expression [| filter_name[:parameter_value] ... ] }} + * ``` * * @param {String} name Name of the filter function to retrieve * @return {Function} the filter function + * @example + + +
    +

    {{ originalText }}

    +

    {{ filteredText }}

    +
    +
    + + + angular.module('filterExample', []) + .controller('MainCtrl', function($scope, $filter) { + $scope.originalText = 'hello'; + $scope.filteredText = $filter('uppercase')($scope.originalText); + }); + +
    */ $FilterProvider.$inject = ['$provide']; + /** @this */ function $FilterProvider($provide) { var suffix = 'Filter'; /** - * @ngdoc function - * @name ng.$controllerProvider#register - * @methodOf ng.$controllerProvider + * @ngdoc method + * @name $filterProvider#register * @param {string|Object} name Name of the filter function, or an object map of filters where * the keys are the filter names and the values are the filter factories. + * + *
    + * **Note:** Filter names must be valid angular {@link expression} identifiers, such as `uppercase` or `orderBy`. + * Names with special characters, such as hyphens and dots, are not allowed. If you wish to namespace + * your filters, then you can use capitalization (`myappSubsectionFilterx`) or underscores + * (`myapp_subsection_filterx`). + *
    + * @param {Function} factory If the first argument was a string, a factory function for the filter to be registered. * @returns {Object} Registered filter instance, or if a map of filters was provided then a map * of the registered filter instances. */ function register(name, factory) { - if(isObject(name)) { + if (isObject(name)) { var filters = {}; forEach(name, function(filter, key) { filters[key] = register(key, filter); @@ -13878,16 +20837,16 @@ //////////////////////////////////////// /* global - currencyFilter: false, - dateFilter: false, - filterFilter: false, - jsonFilter: false, - limitToFilter: false, - lowercaseFilter: false, - numberFilter: false, - orderByFilter: false, - uppercaseFilter: false, - */ + currencyFilter: false, + dateFilter: false, + filterFilter: false, + jsonFilter: false, + limitToFilter: false, + lowercaseFilter: false, + numberFilter: false, + orderByFilter: false, + uppercaseFilter: false + */ register('currency', currencyFilter); register('date', dateFilter); @@ -13902,52 +20861,75 @@ /** * @ngdoc filter - * @name ng.filter:filter - * @function + * @name filter + * @kind function * * @description * Selects a subset of items from `array` and returns it as a new array. * * @param {Array} array The source array. + *
    + * **Note**: If the array contains objects that reference themselves, filtering is not possible. + *
    * @param {string|Object|function()} expression The predicate to be used for selecting items from * `array`. * * Can be one of: * - * - `string`: The string is evaluated as an expression and the resulting value is used for substring match against - * the contents of the `array`. All strings or objects with string properties in `array` that contain this string - * will be returned. The predicate can be negated by prefixing the string with `!`. + * - `string`: The string is used for matching against the contents of the `array`. All strings or + * objects with string properties in `array` that match this string will be returned. This also + * applies to nested object properties. + * The predicate can be negated by prefixing the string with `!`. * * - `Object`: A pattern object can be used to filter specific properties on objects contained * by `array`. For example `{name:"M", phone:"1"}` predicate will return an array of items * which have property `name` containing "M" and property `phone` containing "1". A special - * property name `$` can be used (as in `{$:"text"}`) to accept a match against any - * property of the object. That's equivalent to the simple substring match with a `string` - * as described above. + * property name (`$` by default) can be used (e.g. as in `{$: "text"}`) to accept a match + * against any property of the object or its nested object properties. That's equivalent to the + * simple substring match with a `string` as described above. The special property name can be + * overwritten, using the `anyPropertyKey` parameter. + * The predicate can be negated by prefixing the string with `!`. + * For example `{name: "!M"}` predicate will return an array of items which have property `name` + * not containing "M". * - * - `function(value)`: A predicate function can be used to write arbitrary filters. The function is - * called for each element of `array`. The final result is an array of those elements that - * the predicate returned true for. + * Note that a named property will match properties on the same level only, while the special + * `$` property will match properties on the same level or deeper. E.g. an array item like + * `{name: {first: 'John', last: 'Doe'}}` will **not** be matched by `{name: 'John'}`, but + * **will** be matched by `{$: 'John'}`. * - * @param {function(actual, expected)|true|undefined} comparator Comparator which is used in - * determining if the expected value (from the filter expression) and actual value (from - * the object in the array) should be considered a match. + * - `function(value, index, array)`: A predicate function can be used to write arbitrary filters. + * The function is called for each element of the array, with the element, its index, and + * the entire array itself as arguments. + * + * The final result is an array of those elements that the predicate returned true for. + * + * @param {function(actual, expected)|true|false} [comparator] Comparator which is used in + * determining if values retrieved using `expression` (when it is not a function) should be + * considered a match based on the expected value (from the filter expression) and actual + * value (from the object in the array). * * Can be one of: * - * - `function(actual, expected)`: - * The function will be given the object value and the predicate value to compare and - * should return true if the item should be included in filtered result. + * - `function(actual, expected)`: + * The function will be given the object value and the predicate value to compare and + * should return true if both values should be considered equal. * - * - `true`: A shorthand for `function(actual, expected) { return angular.equals(expected, actual)}`. - * this is essentially strict comparison of expected and actual. + * - `true`: A shorthand for `function(actual, expected) { return angular.equals(actual, expected)}`. + * This is essentially strict comparison of expected and actual. * - * - `false|undefined`: A short hand for a function which will look for a substring match in case - * insensitive way. + * - `false`: A short hand for a function which will look for a substring match in a case + * insensitive way. Primitive values are converted to strings. Objects are not compared against + * primitives, unless they have a custom `toString` method (e.g. `Date` objects). + * + * + * Defaults to `false`. + * + * @param {string} [anyPropertyKey] The special property name that matches against any property. + * By default `$`. * * @example - - + +
    - Search: + @@ -13964,144 +20946,194 @@
    NamePhone

    - Any:
    - Name only
    - Phone only
    - Equality
    +
    +
    +
    +
    - - - + + +
    NamePhone
    {{friend.name}}{{friend.phone}}
    {{friendObj.name}}{{friendObj.phone}}
    -
    - - it('should search across all fields when filtering with a string', function() { - input('searchText').enter('m'); - expect(repeater('#searchTextResults tr', 'friend in friends').column('friend.name')). - toEqual(['Mary', 'Mike', 'Adam']); + + + var expectFriendNames = function(expectedNames, key) { + element.all(by.repeater(key + ' in friends').column(key + '.name')).then(function(arr) { + arr.forEach(function(wd, i) { + expect(wd.getText()).toMatch(expectedNames[i]); + }); + }); + }; - input('searchText').enter('76'); - expect(repeater('#searchTextResults tr', 'friend in friends').column('friend.name')). - toEqual(['John', 'Julie']); + it('should search across all fields when filtering with a string', function() { + var searchText = element(by.model('searchText')); + searchText.clear(); + searchText.sendKeys('m'); + expectFriendNames(['Mary', 'Mike', 'Adam'], 'friend'); + + searchText.clear(); + searchText.sendKeys('76'); + expectFriendNames(['John', 'Julie'], 'friend'); }); it('should search in specific fields when filtering with a predicate object', function() { - input('search.$').enter('i'); - expect(repeater('#searchObjResults tr', 'friend in friends').column('friend.name')). - toEqual(['Mary', 'Mike', 'Julie', 'Juliette']); + var searchAny = element(by.model('search.$')); + searchAny.clear(); + searchAny.sendKeys('i'); + expectFriendNames(['Mary', 'Mike', 'Julie', 'Juliette'], 'friendObj'); }); it('should use a equal comparison when comparator is true', function() { - input('search.name').enter('Julie'); - input('strict').check(); - expect(repeater('#searchObjResults tr', 'friend in friends').column('friend.name')). - toEqual(['Julie']); + var searchName = element(by.model('search.name')); + var strict = element(by.model('strict')); + searchName.clear(); + searchName.sendKeys('Julie'); + strict.click(); + expectFriendNames(['Julie'], 'friendObj'); }); - -
    + + */ + function filterFilter() { - return function(array, expression, comparator) { - if (!isArray(array)) return array; - - var comparatorType = typeof(comparator), - predicates = []; - - predicates.check = function(value) { - for (var j = 0; j < predicates.length; j++) { - if(!predicates[j](value)) { - return false; - } - } - return true; - }; - - if (comparatorType !== 'function') { - if (comparatorType === 'boolean' && comparator) { - comparator = function(obj, text) { - return angular.equals(obj, text); - }; + return function(array, expression, comparator, anyPropertyKey) { + if (!isArrayLike(array)) { + if (array == null) { + return array; } else { - comparator = function(obj, text) { - text = (''+text).toLowerCase(); - return (''+obj).toLowerCase().indexOf(text) > -1; - }; + throw minErr('filter')('notarray', 'Expected array but received: {0}', array); } } - var search = function(obj, text){ - if (typeof text == 'string' && text.charAt(0) === '!') { - return !search(obj, text.substr(1)); - } - switch (typeof obj) { - case "boolean": - case "number": - case "string": - return comparator(obj, text); - case "object": - switch (typeof text) { - case "object": - return comparator(obj, text); - default: - for ( var objKey in obj) { - if (objKey.charAt(0) !== '$' && search(obj[objKey], text)) { - return true; - } - } - break; - } - return false; - case "array": - for ( var i = 0; i < obj.length; i++) { - if (search(obj[i], text)) { - return true; - } - } - return false; - default: - return false; - } - }; - switch (typeof expression) { - case "boolean": - case "number": - case "string": - // Set up expression object and fall through - expression = {$:expression}; - // jshint -W086 - case "object": - // jshint +W086 - for (var key in expression) { - (function(path) { - if (typeof expression[path] == 'undefined') return; - predicates.push(function(value) { - return search(path == '$' ? value : getter(value, path), expression[path]); - }); - })(key); - } - break; + anyPropertyKey = anyPropertyKey || '$'; + var expressionType = getTypeForFilter(expression); + var predicateFn; + var matchAgainstAnyProp; + + switch (expressionType) { case 'function': - predicates.push(expression); + predicateFn = expression; + break; + case 'boolean': + case 'null': + case 'number': + case 'string': + matchAgainstAnyProp = true; + // falls through + case 'object': + predicateFn = createPredicateFn(expression, comparator, anyPropertyKey, matchAgainstAnyProp); break; default: return array; } - var filtered = []; - for ( var j = 0; j < array.length; j++) { - var value = array[j]; - if (predicates.check(value)) { - filtered.push(value); - } - } - return filtered; + + return Array.prototype.filter.call(array, predicateFn); }; } +// Helper functions for `filterFilter` + function createPredicateFn(expression, comparator, anyPropertyKey, matchAgainstAnyProp) { + var shouldMatchPrimitives = isObject(expression) && (anyPropertyKey in expression); + var predicateFn; + + if (comparator === true) { + comparator = equals; + } else if (!isFunction(comparator)) { + comparator = function(actual, expected) { + if (isUndefined(actual)) { + // No substring matching against `undefined` + return false; + } + if ((actual === null) || (expected === null)) { + // No substring matching against `null`; only match against `null` + return actual === expected; + } + if (isObject(expected) || (isObject(actual) && !hasCustomToString(actual))) { + // Should not compare primitives against objects, unless they have custom `toString` method + return false; + } + + actual = lowercase('' + actual); + expected = lowercase('' + expected); + return actual.indexOf(expected) !== -1; + }; + } + + predicateFn = function(item) { + if (shouldMatchPrimitives && !isObject(item)) { + return deepCompare(item, expression[anyPropertyKey], comparator, anyPropertyKey, false); + } + return deepCompare(item, expression, comparator, anyPropertyKey, matchAgainstAnyProp); + }; + + return predicateFn; + } + + function deepCompare(actual, expected, comparator, anyPropertyKey, matchAgainstAnyProp, dontMatchWholeObject) { + var actualType = getTypeForFilter(actual); + var expectedType = getTypeForFilter(expected); + + if ((expectedType === 'string') && (expected.charAt(0) === '!')) { + return !deepCompare(actual, expected.substring(1), comparator, anyPropertyKey, matchAgainstAnyProp); + } else if (isArray(actual)) { + // In case `actual` is an array, consider it a match + // if ANY of it's items matches `expected` + return actual.some(function(item) { + return deepCompare(item, expected, comparator, anyPropertyKey, matchAgainstAnyProp); + }); + } + + switch (actualType) { + case 'object': + var key; + if (matchAgainstAnyProp) { + for (key in actual) { + // Under certain, rare, circumstances, key may not be a string and `charAt` will be undefined + // See: https://github.com/angular/angular.js/issues/15644 + if (key.charAt && (key.charAt(0) !== '$') && + deepCompare(actual[key], expected, comparator, anyPropertyKey, true)) { + return true; + } + } + return dontMatchWholeObject ? false : deepCompare(actual, expected, comparator, anyPropertyKey, false); + } else if (expectedType === 'object') { + for (key in expected) { + var expectedVal = expected[key]; + if (isFunction(expectedVal) || isUndefined(expectedVal)) { + continue; + } + + var matchAnyProperty = key === anyPropertyKey; + var actualVal = matchAnyProperty ? actual : actual[key]; + if (!deepCompare(actualVal, expectedVal, comparator, anyPropertyKey, matchAnyProperty, matchAnyProperty)) { + return false; + } + } + return true; + } else { + return comparator(actual, expected); + } + case 'function': + return false; + default: + return comparator(actual, expected); + } + } + +// Used for easily differentiating between `null` and actual `object` + function getTypeForFilter(val) { + return (val === null) ? 'null' : typeof val; + } + + var MAX_DIGITS = 22; + var DECIMAL_SEP = '.'; + var ZERO_CHAR = '0'; + /** * @ngdoc filter - * @name ng.filter:currency - * @function + * @name currency + * @kind function * * @description * Formats a number as a currency (ie $1,234.56). When no currency symbol is provided, default @@ -14109,235 +21141,441 @@ * * @param {number} amount Input to filter. * @param {string=} symbol Currency symbol or identifier to be displayed. + * @param {number=} fractionSize Number of decimal places to round the amount to, defaults to default max fraction size for current locale * @returns {string} Formatted number. * * * @example - - + + -
    -
    - default currency symbol ($): {{amount | currency}}
    - custom currency identifier (USD$): {{amount | currency:"USD$"}} +
    +
    + default currency symbol ($): {{amount | currency}}
    + custom currency identifier (USD$): {{amount | currency:"USD$"}}
    + no fractions (0): {{amount | currency:"USD$":0}}
    - - + + it('should init with 1234.56', function() { - expect(binding('amount | currency')).toBe('$1,234.56'); - expect(binding('amount | currency:"USD$"')).toBe('USD$1,234.56'); + expect(element(by.id('currency-default')).getText()).toBe('$1,234.56'); + expect(element(by.id('currency-custom')).getText()).toBe('USD$1,234.56'); + expect(element(by.id('currency-no-fractions')).getText()).toBe('USD$1,235'); }); it('should update', function() { - input('amount').enter('-1234'); - expect(binding('amount | currency')).toBe('($1,234.00)'); - expect(binding('amount | currency:"USD$"')).toBe('(USD$1,234.00)'); + if (browser.params.browser === 'safari') { + // Safari does not understand the minus key. See + // https://github.com/angular/protractor/issues/481 + return; + } + element(by.model('amount')).clear(); + element(by.model('amount')).sendKeys('-1234'); + expect(element(by.id('currency-default')).getText()).toBe('-$1,234.00'); + expect(element(by.id('currency-custom')).getText()).toBe('-USD$1,234.00'); + expect(element(by.id('currency-no-fractions')).getText()).toBe('-USD$1,234'); }); - - + + */ currencyFilter.$inject = ['$locale']; function currencyFilter($locale) { var formats = $locale.NUMBER_FORMATS; - return function(amount, currencySymbol){ - if (isUndefined(currencySymbol)) currencySymbol = formats.CURRENCY_SYM; - return formatNumber(amount, formats.PATTERNS[1], formats.GROUP_SEP, formats.DECIMAL_SEP, 2). - replace(/\u00A4/g, currencySymbol); + return function(amount, currencySymbol, fractionSize) { + if (isUndefined(currencySymbol)) { + currencySymbol = formats.CURRENCY_SYM; + } + + if (isUndefined(fractionSize)) { + fractionSize = formats.PATTERNS[1].maxFrac; + } + + // if null or undefined pass it through + return (amount == null) + ? amount + : formatNumber(amount, formats.PATTERNS[1], formats.GROUP_SEP, formats.DECIMAL_SEP, fractionSize). + replace(/\u00A4/g, currencySymbol); }; } /** * @ngdoc filter - * @name ng.filter:number - * @function + * @name number + * @kind function * * @description * Formats a number as text. * + * If the input is null or undefined, it will just be returned. + * If the input is infinite (Infinity or -Infinity), the Infinity symbol '∞' or '-∞' is returned, respectively. * If the input is not a number an empty string is returned. * + * * @param {number|string} number Number to format. * @param {(number|string)=} fractionSize Number of decimal places to round the number to. * If this is not provided then the fraction size is computed from the current locale's number * formatting pattern. In the case of the default locale, it will be 3. - * @returns {string} Number rounded to decimalPlaces and places a “,” after each third digit. + * @returns {string} Number rounded to `fractionSize` appropriately formatted based on the current + * locale (e.g., in the en_US locale it will have "." as the decimal separator and + * include "," group separators after each third digit). * * @example - - + + -
    - Enter number:
    - Default formatting: {{val | number}}
    - No fractions: {{val | number:0}}
    - Negative number: {{-val | number:4}} +
    +
    + Default formatting: {{val | number}}
    + No fractions: {{val | number:0}}
    + Negative number: {{-val | number:4}}
    - - + + it('should format numbers', function() { - expect(binding('val | number')).toBe('1,234.568'); - expect(binding('val | number:0')).toBe('1,235'); - expect(binding('-val | number:4')).toBe('-1,234.5679'); + expect(element(by.id('number-default')).getText()).toBe('1,234.568'); + expect(element(by.binding('val | number:0')).getText()).toBe('1,235'); + expect(element(by.binding('-val | number:4')).getText()).toBe('-1,234.5679'); }); it('should update', function() { - input('val').enter('3374.333'); - expect(binding('val | number')).toBe('3,374.333'); - expect(binding('val | number:0')).toBe('3,374'); - expect(binding('-val | number:4')).toBe('-3,374.3330'); - }); - - + element(by.model('val')).clear(); + element(by.model('val')).sendKeys('3374.333'); + expect(element(by.id('number-default')).getText()).toBe('3,374.333'); + expect(element(by.binding('val | number:0')).getText()).toBe('3,374'); + expect(element(by.binding('-val | number:4')).getText()).toBe('-3,374.3330'); + }); + + */ - - numberFilter.$inject = ['$locale']; function numberFilter($locale) { var formats = $locale.NUMBER_FORMATS; return function(number, fractionSize) { - return formatNumber(number, formats.PATTERNS[0], formats.GROUP_SEP, formats.DECIMAL_SEP, + + // if null or undefined pass it through + return (number == null) + ? number + : formatNumber(number, formats.PATTERNS[0], formats.GROUP_SEP, formats.DECIMAL_SEP, fractionSize); }; } - var DECIMAL_SEP = '.'; - function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) { - if (isNaN(number) || !isFinite(number)) return ''; + /** + * Parse a number (as a string) into three components that can be used + * for formatting the number. + * + * (Significant bits of this parse algorithm came from https://github.com/MikeMcl/big.js/) + * + * @param {string} numStr The number to parse + * @return {object} An object describing this number, containing the following keys: + * - d : an array of digits containing leading zeros as necessary + * - i : the number of the digits in `d` that are to the left of the decimal point + * - e : the exponent for numbers that would need more than `MAX_DIGITS` digits in `d` + * + */ + function parse(numStr) { + var exponent = 0, digits, numberOfIntegerDigits; + var i, j, zeros; - var isNegative = number < 0; - number = Math.abs(number); - var numStr = number + '', - formatedText = '', - parts = []; - - var hasExponent = false; - if (numStr.indexOf('e') !== -1) { - var match = numStr.match(/([\d\.]+)e(-?)(\d+)/); - if (match && match[2] == '-' && match[3] > fractionSize + 1) { - numStr = '0'; - } else { - formatedText = numStr; - hasExponent = true; - } + // Decimal point? + if ((numberOfIntegerDigits = numStr.indexOf(DECIMAL_SEP)) > -1) { + numStr = numStr.replace(DECIMAL_SEP, ''); } - if (!hasExponent) { - var fractionLen = (numStr.split(DECIMAL_SEP)[1] || '').length; + // Exponential form? + if ((i = numStr.search(/e/i)) > 0) { + // Work out the exponent. + if (numberOfIntegerDigits < 0) numberOfIntegerDigits = i; + numberOfIntegerDigits += +numStr.slice(i + 1); + numStr = numStr.substring(0, i); + } else if (numberOfIntegerDigits < 0) { + // There was no decimal point or exponent so it is an integer. + numberOfIntegerDigits = numStr.length; + } - // determine fractionSize if it is not specified - if (isUndefined(fractionSize)) { - fractionSize = Math.min(Math.max(pattern.minFrac, fractionLen), pattern.maxFrac); - } + // Count the number of leading zeros. + for (i = 0; numStr.charAt(i) === ZERO_CHAR; i++) { /* empty */ } - var pow = Math.pow(10, fractionSize); - number = Math.round(number * pow) / pow; - var fraction = ('' + number).split(DECIMAL_SEP); - var whole = fraction[0]; - fraction = fraction[1] || ''; - - var i, pos = 0, - lgroup = pattern.lgSize, - group = pattern.gSize; - - if (whole.length >= (lgroup + group)) { - pos = whole.length - lgroup; - for (i = 0; i < pos; i++) { - if ((pos - i)%group === 0 && i !== 0) { - formatedText += groupSep; - } - formatedText += whole.charAt(i); - } - } - - for (i = pos; i < whole.length; i++) { - if ((whole.length - i)%lgroup === 0 && i !== 0) { - formatedText += groupSep; - } - formatedText += whole.charAt(i); - } - - // format fraction part. - while(fraction.length < fractionSize) { - fraction += '0'; - } - - if (fractionSize && fractionSize !== "0") formatedText += decimalSep + fraction.substr(0, fractionSize); + if (i === (zeros = numStr.length)) { + // The digits are all zero. + digits = [0]; + numberOfIntegerDigits = 1; } else { + // Count the number of trailing zeros + zeros--; + while (numStr.charAt(zeros) === ZERO_CHAR) zeros--; - if (fractionSize > 0 && number > -1 && number < 1) { - formatedText = number.toFixed(fractionSize); + // Trailing zeros are insignificant so ignore them + numberOfIntegerDigits -= i; + digits = []; + // Convert string to array of digits without leading/trailing zeros. + for (j = 0; i <= zeros; i++, j++) { + digits[j] = +numStr.charAt(i); } } - parts.push(isNegative ? pattern.negPre : pattern.posPre); - parts.push(formatedText); - parts.push(isNegative ? pattern.negSuf : pattern.posSuf); - return parts.join(''); + // If the number overflows the maximum allowed digits then use an exponent. + if (numberOfIntegerDigits > MAX_DIGITS) { + digits = digits.splice(0, MAX_DIGITS - 1); + exponent = numberOfIntegerDigits - 1; + numberOfIntegerDigits = 1; + } + + return { d: digits, e: exponent, i: numberOfIntegerDigits }; } - function padNumber(num, digits, trim) { + /** + * Round the parsed number to the specified number of decimal places + * This function changed the parsedNumber in-place + */ + function roundNumber(parsedNumber, fractionSize, minFrac, maxFrac) { + var digits = parsedNumber.d; + var fractionLen = digits.length - parsedNumber.i; + + // determine fractionSize if it is not specified; `+fractionSize` converts it to a number + fractionSize = (isUndefined(fractionSize)) ? Math.min(Math.max(minFrac, fractionLen), maxFrac) : +fractionSize; + + // The index of the digit to where rounding is to occur + var roundAt = fractionSize + parsedNumber.i; + var digit = digits[roundAt]; + + if (roundAt > 0) { + // Drop fractional digits beyond `roundAt` + digits.splice(Math.max(parsedNumber.i, roundAt)); + + // Set non-fractional digits beyond `roundAt` to 0 + for (var j = roundAt; j < digits.length; j++) { + digits[j] = 0; + } + } else { + // We rounded to zero so reset the parsedNumber + fractionLen = Math.max(0, fractionLen); + parsedNumber.i = 1; + digits.length = Math.max(1, roundAt = fractionSize + 1); + digits[0] = 0; + for (var i = 1; i < roundAt; i++) digits[i] = 0; + } + + if (digit >= 5) { + if (roundAt - 1 < 0) { + for (var k = 0; k > roundAt; k--) { + digits.unshift(0); + parsedNumber.i++; + } + digits.unshift(1); + parsedNumber.i++; + } else { + digits[roundAt - 1]++; + } + } + + // Pad out with zeros to get the required fraction length + for (; fractionLen < Math.max(0, fractionSize); fractionLen++) digits.push(0); + + + // Do any carrying, e.g. a digit was rounded up to 10 + var carry = digits.reduceRight(function(carry, d, i, digits) { + d = d + carry; + digits[i] = d % 10; + return Math.floor(d / 10); + }, 0); + if (carry) { + digits.unshift(carry); + parsedNumber.i++; + } + } + + /** + * Format a number into a string + * @param {number} number The number to format + * @param {{ + * minFrac, // the minimum number of digits required in the fraction part of the number + * maxFrac, // the maximum number of digits required in the fraction part of the number + * gSize, // number of digits in each group of separated digits + * lgSize, // number of digits in the last group of digits before the decimal separator + * negPre, // the string to go in front of a negative number (e.g. `-` or `(`)) + * posPre, // the string to go in front of a positive number + * negSuf, // the string to go after a negative number (e.g. `)`) + * posSuf // the string to go after a positive number + * }} pattern + * @param {string} groupSep The string to separate groups of number (e.g. `,`) + * @param {string} decimalSep The string to act as the decimal separator (e.g. `.`) + * @param {[type]} fractionSize The size of the fractional part of the number + * @return {string} The number formatted as a string + */ + function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) { + + if (!(isString(number) || isNumber(number)) || isNaN(number)) return ''; + + var isInfinity = !isFinite(number); + var isZero = false; + var numStr = Math.abs(number) + '', + formattedText = '', + parsedNumber; + + if (isInfinity) { + formattedText = '\u221e'; + } else { + parsedNumber = parse(numStr); + + roundNumber(parsedNumber, fractionSize, pattern.minFrac, pattern.maxFrac); + + var digits = parsedNumber.d; + var integerLen = parsedNumber.i; + var exponent = parsedNumber.e; + var decimals = []; + isZero = digits.reduce(function(isZero, d) { return isZero && !d; }, true); + + // pad zeros for small numbers + while (integerLen < 0) { + digits.unshift(0); + integerLen++; + } + + // extract decimals digits + if (integerLen > 0) { + decimals = digits.splice(integerLen, digits.length); + } else { + decimals = digits; + digits = [0]; + } + + // format the integer digits with grouping separators + var groups = []; + if (digits.length >= pattern.lgSize) { + groups.unshift(digits.splice(-pattern.lgSize, digits.length).join('')); + } + while (digits.length > pattern.gSize) { + groups.unshift(digits.splice(-pattern.gSize, digits.length).join('')); + } + if (digits.length) { + groups.unshift(digits.join('')); + } + formattedText = groups.join(groupSep); + + // append the decimal digits + if (decimals.length) { + formattedText += decimalSep + decimals.join(''); + } + + if (exponent) { + formattedText += 'e+' + exponent; + } + } + if (number < 0 && !isZero) { + return pattern.negPre + formattedText + pattern.negSuf; + } else { + return pattern.posPre + formattedText + pattern.posSuf; + } + } + + function padNumber(num, digits, trim, negWrap) { var neg = ''; - if (num < 0) { - neg = '-'; - num = -num; + if (num < 0 || (negWrap && num <= 0)) { + if (negWrap) { + num = -num + 1; + } else { + num = -num; + neg = '-'; + } } num = '' + num; - while(num.length < digits) num = '0' + num; - if (trim) + while (num.length < digits) num = ZERO_CHAR + num; + if (trim) { num = num.substr(num.length - digits); + } return neg + num; } - function dateGetter(name, size, offset, trim) { + function dateGetter(name, size, offset, trim, negWrap) { offset = offset || 0; return function(date) { var value = date['get' + name](); - if (offset > 0 || value > -offset) + if (offset > 0 || value > -offset) { value += offset; - if (value === 0 && offset == -12 ) value = 12; - return padNumber(value, size, trim); + } + if (value === 0 && offset === -12) value = 12; + return padNumber(value, size, trim, negWrap); }; } - function dateStrGetter(name, shortForm) { + function dateStrGetter(name, shortForm, standAlone) { return function(date, formats) { var value = date['get' + name](); - var get = uppercase(shortForm ? ('SHORT' + name) : name); + var propPrefix = (standAlone ? 'STANDALONE' : '') + (shortForm ? 'SHORT' : ''); + var get = uppercase(propPrefix + name); return formats[get][value]; }; } - function timeZoneGetter(date) { - var zone = -1 * date.getTimezoneOffset(); - var paddedZone = (zone >= 0) ? "+" : ""; + function timeZoneGetter(date, formats, offset) { + var zone = -1 * offset; + var paddedZone = (zone >= 0) ? '+' : ''; paddedZone += padNumber(Math[zone > 0 ? 'floor' : 'ceil'](zone / 60), 2) + - padNumber(Math.abs(zone % 60), 2); + padNumber(Math.abs(zone % 60), 2); return paddedZone; } + function getFirstThursdayOfYear(year) { + // 0 = index of January + var dayOfWeekOnFirst = (new Date(year, 0, 1)).getDay(); + // 4 = index of Thursday (+1 to account for 1st = 5) + // 11 = index of *next* Thursday (+1 account for 1st = 12) + return new Date(year, 0, ((dayOfWeekOnFirst <= 4) ? 5 : 12) - dayOfWeekOnFirst); + } + + function getThursdayThisWeek(datetime) { + return new Date(datetime.getFullYear(), datetime.getMonth(), + // 4 = index of Thursday + datetime.getDate() + (4 - datetime.getDay())); + } + + function weekGetter(size) { + return function(date) { + var firstThurs = getFirstThursdayOfYear(date.getFullYear()), + thisThurs = getThursdayThisWeek(date); + + var diff = +thisThurs - +firstThurs, + result = 1 + Math.round(diff / 6.048e8); // 6.048e8 ms per week + + return padNumber(result, size); + }; + } + function ampmGetter(date, formats) { return date.getHours() < 12 ? formats.AMPMS[0] : formats.AMPMS[1]; } + function eraGetter(date, formats) { + return date.getFullYear() <= 0 ? formats.ERAS[0] : formats.ERAS[1]; + } + + function longEraGetter(date, formats) { + return date.getFullYear() <= 0 ? formats.ERANAMES[0] : formats.ERANAMES[1]; + } + var DATE_FORMATS = { - yyyy: dateGetter('FullYear', 4), - yy: dateGetter('FullYear', 2, 0, true), - y: dateGetter('FullYear', 1), + yyyy: dateGetter('FullYear', 4, 0, false, true), + yy: dateGetter('FullYear', 2, 0, true, true), + y: dateGetter('FullYear', 1, 0, false, true), MMMM: dateStrGetter('Month'), MMM: dateStrGetter('Month', true), MM: dateGetter('Month', 2, 1), M: dateGetter('Month', 1, 1), + LLLL: dateStrGetter('Month', false, true), dd: dateGetter('Date', 2), d: dateGetter('Date', 1), HH: dateGetter('Hours', 2), @@ -14354,16 +21592,22 @@ EEEE: dateStrGetter('Day'), EEE: dateStrGetter('Day', true), a: ampmGetter, - Z: timeZoneGetter + Z: timeZoneGetter, + ww: weekGetter(2), + w: weekGetter(1), + G: eraGetter, + GG: eraGetter, + GGG: eraGetter, + GGGG: longEraGetter }; - var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z))(.*)/, - NUMBER_STRING = /^\-?\d+$/; + var DATE_FORMATS_SPLIT = /((?:[^yMLdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|L+|d+|H+|h+|m+|s+|a|Z|G+|w+))([\s\S]*)/, + NUMBER_STRING = /^-?\d+$/; /** * @ngdoc filter - * @name ng.filter:date - * @function + * @name date + * @kind function * * @description * Formats `date` to a string based on the requested `format`. @@ -14377,69 +21621,84 @@ * * `'MMM'`: Month in year (Jan-Dec) * * `'MM'`: Month in year, padded (01-12) * * `'M'`: Month in year (1-12) + * * `'LLLL'`: Stand-alone month in year (January-December) * * `'dd'`: Day in month, padded (01-31) * * `'d'`: Day in month (1-31) * * `'EEEE'`: Day in Week,(Sunday-Saturday) * * `'EEE'`: Day in Week, (Sun-Sat) * * `'HH'`: Hour in day, padded (00-23) * * `'H'`: Hour in day (0-23) - * * `'hh'`: Hour in am/pm, padded (01-12) - * * `'h'`: Hour in am/pm, (1-12) + * * `'hh'`: Hour in AM/PM, padded (01-12) + * * `'h'`: Hour in AM/PM, (1-12) * * `'mm'`: Minute in hour, padded (00-59) * * `'m'`: Minute in hour (0-59) * * `'ss'`: Second in minute, padded (00-59) * * `'s'`: Second in minute (0-59) - * * `'.sss' or ',sss'`: Millisecond in second, padded (000-999) - * * `'a'`: am/pm marker + * * `'sss'`: Millisecond in second, padded (000-999) + * * `'a'`: AM/PM marker * * `'Z'`: 4 digit (+sign) representation of the timezone offset (-1200-+1200) + * * `'ww'`: Week of year, padded (00-53). Week 01 is the week with the first Thursday of the year + * * `'w'`: Week of year (0-53). Week 1 is the week with the first Thursday of the year + * * `'G'`, `'GG'`, `'GGG'`: The abbreviated form of the era string (e.g. 'AD') + * * `'GGGG'`: The long form of the era string (e.g. 'Anno Domini') * * `format` string can also be one of the following predefined * {@link guide/i18n localizable formats}: * * * `'medium'`: equivalent to `'MMM d, y h:mm:ss a'` for en_US locale - * (e.g. Sep 3, 2010 12:05:08 pm) - * * `'short'`: equivalent to `'M/d/yy h:mm a'` for en_US locale (e.g. 9/3/10 12:05 pm) - * * `'fullDate'`: equivalent to `'EEEE, MMMM d,y'` for en_US locale + * (e.g. Sep 3, 2010 12:05:08 PM) + * * `'short'`: equivalent to `'M/d/yy h:mm a'` for en_US locale (e.g. 9/3/10 12:05 PM) + * * `'fullDate'`: equivalent to `'EEEE, MMMM d, y'` for en_US locale * (e.g. Friday, September 3, 2010) * * `'longDate'`: equivalent to `'MMMM d, y'` for en_US locale (e.g. September 3, 2010) * * `'mediumDate'`: equivalent to `'MMM d, y'` for en_US locale (e.g. Sep 3, 2010) * * `'shortDate'`: equivalent to `'M/d/yy'` for en_US locale (e.g. 9/3/10) - * * `'mediumTime'`: equivalent to `'h:mm:ss a'` for en_US locale (e.g. 12:05:08 pm) - * * `'shortTime'`: equivalent to `'h:mm a'` for en_US locale (e.g. 12:05 pm) + * * `'mediumTime'`: equivalent to `'h:mm:ss a'` for en_US locale (e.g. 12:05:08 PM) + * * `'shortTime'`: equivalent to `'h:mm a'` for en_US locale (e.g. 12:05 PM) * - * `format` string can contain literal values. These need to be quoted with single quotes (e.g. - * `"h 'in the morning'"`). In order to output single quote, use two single quotes in a sequence + * `format` string can contain literal values. These need to be escaped by surrounding with single quotes (e.g. + * `"h 'in the morning'"`). In order to output a single quote, escape it - i.e., two single quotes in a sequence * (e.g. `"h 'o''clock'"`). * + * Any other characters in the `format` string will be output as-is. + * * @param {(Date|number|string)} date Date to format either as Date object, milliseconds (string or - * number) or various ISO 8601 datetime string formats (e.g. yyyy-MM-ddTHH:mm:ss.SSSZ and its + * number) or various ISO 8601 datetime string formats (e.g. yyyy-MM-ddTHH:mm:ss.sssZ and its * shorter versions like yyyy-MM-ddTHH:mmZ, yyyy-MM-dd or yyyyMMddTHHmmssZ). If no timezone is * specified in the string input, the time is considered to be in the local timezone. * @param {string=} format Formatting rules (see Description). If not specified, * `mediumDate` is used. + * @param {string=} timezone Timezone to be used for formatting. 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. * @returns {string} Formatted string or the input if input is not recognized as date/millis. * * @example - - + + {{1288323623006 | date:'medium'}}: - {{1288323623006 | date:'medium'}}
    + {{1288323623006 | date:'medium'}}
    {{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}: - {{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}
    + {{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}
    {{1288323623006 | date:'MM/dd/yyyy @ h:mma'}}: - {{'1288323623006' | date:'MM/dd/yyyy @ h:mma'}}
    -
    - + {{'1288323623006' | date:'MM/dd/yyyy @ h:mma'}}
    + {{1288323623006 | date:"MM/dd/yyyy 'at' h:mma"}}: + {{'1288323623006' | date:"MM/dd/yyyy 'at' h:mma"}}
    + + it('should format date', function() { - expect(binding("1288323623006 | date:'medium'")). + expect(element(by.binding("1288323623006 | date:'medium'")).getText()). toMatch(/Oct 2\d, 2010 \d{1,2}:\d{2}:\d{2} (AM|PM)/); - expect(binding("1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'")). - toMatch(/2010\-10\-2\d \d{2}:\d{2}:\d{2} (\-|\+)?\d{4}/); - expect(binding("'1288323623006' | date:'MM/dd/yyyy @ h:mma'")). - toMatch(/10\/2\d\/2010 @ \d{1,2}:\d{2}(AM|PM)/); - }); -
    -
    + expect(element(by.binding("1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'")).getText()). + toMatch(/2010-10-2\d \d{2}:\d{2}:\d{2} (-|\+)?\d{4}/); + expect(element(by.binding("'1288323623006' | date:'MM/dd/yyyy @ h:mma'")).getText()). + toMatch(/10\/2\d\/2010 @ \d{1,2}:\d{2}(AM|PM)/); + expect(element(by.binding("'1288323623006' | date:\"MM/dd/yyyy 'at' h:mma\"")).getText()). + toMatch(/10\/2\d\/2010 at \d{1,2}:\d{2}(AM|PM)/); + }); + + */ dateFilter.$inject = ['$locale']; function dateFilter($locale) { @@ -14449,22 +21708,22 @@ // 1 2 3 4 5 6 7 8 9 10 11 function jsonStringToDate(string) { var match; - if (match = string.match(R_ISO8601_STR)) { + if ((match = string.match(R_ISO8601_STR))) { var date = new Date(0), - tzHour = 0, - tzMin = 0, - dateSetter = match[8] ? date.setUTCFullYear : date.setFullYear, - timeSetter = match[8] ? date.setUTCHours : date.setHours; + tzHour = 0, + tzMin = 0, + dateSetter = match[8] ? date.setUTCFullYear : date.setFullYear, + timeSetter = match[8] ? date.setUTCHours : date.setHours; if (match[9]) { - tzHour = int(match[9] + match[10]); - tzMin = int(match[9] + match[11]); + tzHour = toInt(match[9] + match[10]); + tzMin = toInt(match[9] + match[11]); } - dateSetter.call(date, int(match[1]), int(match[2]) - 1, int(match[3])); - var h = int(match[4]||0) - tzHour; - var m = int(match[5]||0) - tzMin; - var s = int(match[6]||0); - var ms = Math.round(parseFloat('0.' + (match[7]||0)) * 1000); + dateSetter.call(date, toInt(match[1]), toInt(match[2]) - 1, toInt(match[3])); + var h = toInt(match[4] || 0) - tzHour; + var m = toInt(match[5] || 0) - tzMin; + var s = toInt(match[6] || 0); + var ms = Math.round(parseFloat('0.' + (match[7] || 0)) * 1000); timeSetter.call(date, h, m, s, ms); return date; } @@ -14472,30 +21731,26 @@ } - return function(date, format) { + return function(date, format, timezone) { var text = '', - parts = [], - fn, match; + parts = [], + fn, match; format = format || 'mediumDate'; format = $locale.DATETIME_FORMATS[format] || format; if (isString(date)) { - if (NUMBER_STRING.test(date)) { - date = int(date); - } else { - date = jsonStringToDate(date); - } + date = NUMBER_STRING.test(date) ? toInt(date) : jsonStringToDate(date); } if (isNumber(date)) { date = new Date(date); } - if (!isDate(date)) { + if (!isDate(date) || !isFinite(date.getTime())) { return date; } - while(format) { + while (format) { match = DATE_FORMATS_SPLIT.exec(format); if (match) { parts = concat(parts, match, 1); @@ -14506,10 +21761,15 @@ } } - forEach(parts, function(value){ + var dateTimezoneOffset = date.getTimezoneOffset(); + if (timezone) { + dateTimezoneOffset = timezoneToOffset(timezone, dateTimezoneOffset); + date = convertTimezoneToLocal(date, timezone, true); + } + forEach(parts, function(value) { fn = DATE_FORMATS[value]; - text += fn ? fn(date, $locale.DATETIME_FORMATS) - : value.replace(/(^'|'$)/g, '').replace(/''/g, "'"); + text += fn ? fn(date, $locale.DATETIME_FORMATS, dateTimezoneOffset) + : value === '\'\'' ? '\'' : value.replace(/(^'|'$)/g, '').replace(/''/g, '\''); }); return text; @@ -14519,8 +21779,8 @@ /** * @ngdoc filter - * @name ng.filter:json - * @function + * @name json + * @kind function * * @description * Allows you to convert a JavaScript object into JSON string. @@ -14529,35 +21789,44 @@ * the binding is automatically converted to JSON. * * @param {*} object Any JavaScript object (including arrays and primitive types) to filter. + * @param {number=} spacing The number of spaces to use per indentation, defaults to 2. * @returns {string} JSON string. * * - * @example: - - -
    {{ {'name':'value'} | json }}
    -
    - + * @example + + +
    {{ {'name':'value'} | json }}
    +
    {{ {'name':'value'} | json:4 }}
    +
    + it('should jsonify filtered objects', function() { - expect(binding("{'name':'value'}")).toMatch(/\{\n "name": ?"value"\n}/); + expect(element(by.id('default-spacing')).getText()).toMatch(/\{\n {2}"name": ?"value"\n}/); + expect(element(by.id('custom-spacing')).getText()).toMatch(/\{\n {4}"name": ?"value"\n}/); }); -
    -
    + + * */ function jsonFilter() { - return function(object) { - return toJson(object, true); + return function(object, spacing) { + if (isUndefined(spacing)) { + spacing = 2; + } + return toJson(object, spacing); }; } /** * @ngdoc filter - * @name ng.filter:lowercase - * @function + * @name lowercase + * @kind function * @description * Converts string to lowercase. + * + * See the {@link ng.uppercase uppercase filter documentation} for a functionally identical example. + * * @see angular.lowercase */ var lowercaseFilter = valueFn(lowercase); @@ -14565,247 +21834,857 @@ /** * @ngdoc filter - * @name ng.filter:uppercase - * @function + * @name uppercase + * @kind function * @description * Converts string to uppercase. - * @see angular.uppercase + * @example + + + +
    + +

    {{title}}

    + +

    {{title | uppercase}}

    +
    +
    +
    */ var uppercaseFilter = valueFn(uppercase); /** - * @ngdoc function - * @name ng.filter:limitTo - * @function + * @ngdoc filter + * @name limitTo + * @kind function * * @description - * Creates a new array or string containing only a specified number of elements. The elements - * are taken from either the beginning or the end of the source array or string, as specified by - * the value and sign (positive or negative) of `limit`. + * Creates a new array or string containing only a specified number of elements. The elements are + * taken from either the beginning or the end of the source array, string or number, as specified by + * the value and sign (positive or negative) of `limit`. Other array-like objects are also supported + * (e.g. array subclasses, NodeLists, jqLite/jQuery collections etc). If a number is used as input, + * it is converted to a string. * - * @param {Array|string} input Source array or string to be limited. - * @param {string|number} limit The length of the returned array or string. If the `limit` number + * @param {Array|ArrayLike|string|number} input - Array/array-like, string or number to be limited. + * @param {string|number} limit - The length of the returned array or string. If the `limit` number * is positive, `limit` number of items from the beginning of the source array/string are copied. * If the number is negative, `limit` number of items from the end of the source array/string - * are copied. The `limit` will be trimmed if it exceeds `array.length` - * @returns {Array|string} A new sub-array or substring of length `limit` or less if input array - * had less than `limit` elements. + * are copied. The `limit` will be trimmed if it exceeds `array.length`. If `limit` is undefined, + * the input will be returned unchanged. + * @param {(string|number)=} begin - Index at which to begin limitation. As a negative index, + * `begin` indicates an offset from the end of `input`. Defaults to `0`. + * @returns {Array|string} A new sub-array or substring of length `limit` or less if the input had + * less than `limit` elements. * * @example - - + + -
    - Limit {{numbers}} to: +
    +

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

    - Limit {{letters}} to: +

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

    + +

    Output long number: {{ longNumber | limitTo:longNumberLimit }}

    - - + + + var numLimitInput = element(by.model('numLimit')); + var letterLimitInput = element(by.model('letterLimit')); + var longNumberLimitInput = element(by.model('longNumberLimit')); + var limitedNumbers = element(by.binding('numbers | limitTo:numLimit')); + var limitedLetters = element(by.binding('letters | limitTo:letterLimit')); + var limitedLongNumber = element(by.binding('longNumber | limitTo:longNumberLimit')); + it('should limit the number array to first three items', function() { - expect(element('.doc-example-live input[ng-model=numLimit]').val()).toBe('3'); - expect(element('.doc-example-live input[ng-model=letterLimit]').val()).toBe('3'); - expect(binding('numbers | limitTo:numLimit')).toEqual('[1,2,3]'); - expect(binding('letters | limitTo:letterLimit')).toEqual('abc'); + expect(numLimitInput.getAttribute('value')).toBe('3'); + expect(letterLimitInput.getAttribute('value')).toBe('3'); + expect(longNumberLimitInput.getAttribute('value')).toBe('3'); + expect(limitedNumbers.getText()).toEqual('Output numbers: [1,2,3]'); + expect(limitedLetters.getText()).toEqual('Output letters: abc'); + expect(limitedLongNumber.getText()).toEqual('Output long number: 234'); }); - it('should update the output when -3 is entered', function() { - input('numLimit').enter(-3); - input('letterLimit').enter(-3); - expect(binding('numbers | limitTo:numLimit')).toEqual('[7,8,9]'); - expect(binding('letters | limitTo:letterLimit')).toEqual('ghi'); - }); + // There is a bug in safari and protractor that doesn't like the minus key + // it('should update the output when -3 is entered', function() { + // numLimitInput.clear(); + // numLimitInput.sendKeys('-3'); + // letterLimitInput.clear(); + // letterLimitInput.sendKeys('-3'); + // longNumberLimitInput.clear(); + // longNumberLimitInput.sendKeys('-3'); + // expect(limitedNumbers.getText()).toEqual('Output numbers: [7,8,9]'); + // expect(limitedLetters.getText()).toEqual('Output letters: ghi'); + // expect(limitedLongNumber.getText()).toEqual('Output long number: 342'); + // }); it('should not exceed the maximum size of input array', function() { - input('numLimit').enter(100); - input('letterLimit').enter(100); - expect(binding('numbers | limitTo:numLimit')).toEqual('[1,2,3,4,5,6,7,8,9]'); - expect(binding('letters | limitTo:letterLimit')).toEqual('abcdefghi'); + numLimitInput.clear(); + numLimitInput.sendKeys('100'); + letterLimitInput.clear(); + letterLimitInput.sendKeys('100'); + longNumberLimitInput.clear(); + longNumberLimitInput.sendKeys('100'); + expect(limitedNumbers.getText()).toEqual('Output numbers: [1,2,3,4,5,6,7,8,9]'); + expect(limitedLetters.getText()).toEqual('Output letters: abcdefghi'); + expect(limitedLongNumber.getText()).toEqual('Output long number: 2345432342'); }); - - + + */ - function limitToFilter(){ - return function(input, limit) { - if (!isArray(input) && !isString(input)) return input; + function limitToFilter() { + return function(input, limit, begin) { + if (Math.abs(Number(limit)) === Infinity) { + limit = Number(limit); + } else { + limit = toInt(limit); + } + if (isNumberNaN(limit)) return input; - limit = int(limit); + if (isNumber(input)) input = input.toString(); + if (!isArrayLike(input)) return input; - if (isString(input)) { - //NaN check on limit - if (limit) { - return limit >= 0 ? input.slice(0, limit) : input.slice(limit, input.length); + begin = (!begin || isNaN(begin)) ? 0 : toInt(begin); + begin = (begin < 0) ? Math.max(0, input.length + begin) : begin; + + if (limit >= 0) { + return sliceFn(input, begin, begin + limit); + } else { + if (begin === 0) { + return sliceFn(input, limit, input.length); } else { - return ""; + return sliceFn(input, Math.max(0, begin + limit), begin); } } - - var out = [], - i, n; - - // if abs(limit) exceeds maximum length, trim it - if (limit > input.length) - limit = input.length; - else if (limit < -input.length) - limit = -input.length; - - if (limit > 0) { - i = 0; - n = limit; - } else { - i = input.length + limit; - n = input.length; - } - - for (; i} expression A predicate to be - * used by the comparator to determine the order of elements. + * For example, `[{id: 'foo'}, {id: 'bar'}] | orderBy:'id'` would result in + * `[{id: 'bar'}, {id: 'foo'}]`. + * + * The `collection` can be an Array or array-like object (e.g. NodeList, jQuery object, TypedArray, + * String, etc). + * + * The `expression` can be a single predicate, or a list of predicates each serving as a tie-breaker + * for the preceding one. The `expression` is evaluated against each item and the output is used + * for comparing with other items. + * + * You can change the sorting order by setting `reverse` to `true`. By default, items are sorted in + * ascending order. + * + * The comparison is done using the `comparator` function. If none is specified, a default, built-in + * comparator is used (see below for details - in a nutshell, it compares numbers numerically and + * strings alphabetically). + * + * ### Under the hood + * + * Ordering the specified `collection` happens in two phases: + * + * 1. All items are passed through the predicate (or predicates), and the returned values are saved + * along with their type (`string`, `number` etc). For example, an item `{label: 'foo'}`, passed + * through a predicate that extracts the value of the `label` property, would be transformed to: + * ``` + * { + * value: 'foo', + * type: 'string', + * index: ... + * } + * ``` + * 2. The comparator function is used to sort the items, based on the derived values, types and + * indices. + * + * If you use a custom comparator, it will be called with pairs of objects of the form + * `{value: ..., type: '...', index: ...}` and is expected to return `0` if the objects are equal + * (as far as the comparator is concerned), `-1` if the 1st one should be ranked higher than the + * second, or `1` otherwise. + * + * In order to ensure that the sorting will be deterministic across platforms, if none of the + * specified predicates can distinguish between two items, `orderBy` will automatically introduce a + * dummy predicate that returns the item's index as `value`. + * (If you are using a custom comparator, make sure it can handle this predicate as well.) + * + * If a custom comparator still can't distinguish between two items, then they will be sorted based + * on their index using the built-in comparator. + * + * Finally, in an attempt to simplify things, if a predicate returns an object as the extracted + * value for an item, `orderBy` will try to convert that object to a primitive value, before passing + * it to the comparator. The following rules govern the conversion: + * + * 1. If the object has a `valueOf()` method that returns a primitive, its return value will be + * used instead.
    + * (If the object has a `valueOf()` method that returns another object, then the returned object + * will be used in subsequent steps.) + * 2. If the object has a custom `toString()` method (i.e. not the one inherited from `Object`) that + * returns a primitive, its return value will be used instead.
    + * (If the object has a `toString()` method that returns another object, then the returned object + * will be used in subsequent steps.) + * 3. No conversion; the object itself is used. + * + * ### The default comparator + * + * The default, built-in comparator should be sufficient for most usecases. In short, it compares + * numbers numerically, strings alphabetically (and case-insensitively), for objects falls back to + * using their index in the original collection, and sorts values of different types by type. + * + * More specifically, it follows these steps to determine the relative order of items: + * + * 1. If the compared values are of different types, compare the types themselves alphabetically. + * 2. If both values are of type `string`, compare them alphabetically in a case- and + * locale-insensitive way. + * 3. If both values are objects, compare their indices instead. + * 4. Otherwise, return: + * - `0`, if the values are equal (by strict equality comparison, i.e. using `===`). + * - `-1`, if the 1st value is "less than" the 2nd value (compared using the `<` operator). + * - `1`, otherwise. + * + * **Note:** If you notice numbers not being sorted as expected, make sure they are actually being + * saved as numbers and not strings. + * **Note:** For the purpose of sorting, `null` values are treated as the string `'null'` (i.e. + * `type: 'string'`, `value: 'null'`). This may cause unexpected sort order relative to + * other values. + * + * @param {Array|ArrayLike} collection - The collection (array or array-like object) to sort. + * @param {(Function|string|Array.)=} expression - A predicate (or list of + * predicates) to be used by the comparator to determine the order of elements. * * Can be one of: * - * - `function`: Getter function. The result of this function will be sorted using the - * `<`, `=`, `>` operator. - * - `string`: An Angular expression which evaluates to an object to order by, such as 'name' - * to sort by a property called 'name'. Optionally prefixed with `+` or `-` to control - * ascending or descending sort order (for example, +name or -name). - * - `Array`: An array of function or string predicates. The first predicate in the array - * is used for sorting, but when two items are equivalent, the next predicate is used. + * - `Function`: A getter function. This function will be called with each item as argument and + * the return value will be used for sorting. + * - `string`: An Angular expression. This expression will be evaluated against each item and the + * result will be used for sorting. For example, use `'label'` to sort by a property called + * `label` or `'label.substring(0, 3)'` to sort by the first 3 characters of the `label` + * property.
    + * (The result of a constant expression is interpreted as a property name to be used for + * comparison. For example, use `'"special name"'` (note the extra pair of quotes) to sort by a + * property called `special name`.)
    + * An expression can be optionally prefixed with `+` or `-` to control the sorting direction, + * ascending or descending. For example, `'+label'` or `'-label'`. If no property is provided, + * (e.g. `'+'` or `'-'`), the collection element itself is used in comparisons. + * - `Array`: An array of function and/or string predicates. If a predicate cannot determine the + * relative order of two items, the next predicate is used as a tie-breaker. + * + * **Note:** If the predicate is missing or empty then it defaults to `'+'`. + * + * @param {boolean=} reverse - If `true`, reverse the sorting order. + * @param {(Function)=} comparator - The comparator function used to determine the relative order of + * value pairs. If omitted, the built-in comparator will be used. + * + * @returns {Array} - The sorted array. * - * @param {boolean=} reverse Reverse the order the array. - * @returns {Array} Sorted copy of the source array. * * @example - - - -
    -
    Sorting predicate = {{predicate}}; reverse = {{reverse}}
    -
    - [ unsorted ] - + * ### Ordering a table with `ngRepeat` + * + * The example below demonstrates a simple {@link ngRepeat ngRepeat}, where the data is sorted by + * age in descending order (expression is set to `'-age'`). The `comparator` is not set, which means + * it defaults to the built-in comparator. + * + + +
    +
    - - - + + + - +
    Name - (^)Phone NumberAgeNamePhone NumberAge
    {{friend.name}} {{friend.phone}} {{friend.age}}
    -
    - - it('should be reverse ordered by aged', function() { - expect(binding('predicate')).toBe('-age'); - expect(repeater('table.friend', 'friend in friends').column('friend.age')). - toEqual(['35', '29', '21', '19', '10']); - expect(repeater('table.friend', 'friend in friends').column('friend.name')). - toEqual(['Adam', 'Julie', 'Mike', 'Mary', 'John']); + + + angular.module('orderByExample1', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.friends = [ + {name: 'John', phone: '555-1212', age: 10}, + {name: 'Mary', phone: '555-9876', age: 19}, + {name: 'Mike', phone: '555-4321', age: 21}, + {name: 'Adam', phone: '555-5678', age: 35}, + {name: 'Julie', phone: '555-8765', age: 29} + ]; + }]); + + + .friends { + border-collapse: collapse; + } + + .friends th { + border-bottom: 1px solid; + } + .friends td, .friends th { + border-left: 1px solid; + padding: 5px 10px; + } + .friends td:first-child, .friends th:first-child { + border-left: none; + } + + + // Element locators + var names = element.all(by.repeater('friends').column('friend.name')); + + it('should sort friends by age in reverse order', function() { + expect(names.get(0).getText()).toBe('Adam'); + expect(names.get(1).getText()).toBe('Julie'); + expect(names.get(2).getText()).toBe('Mike'); + expect(names.get(3).getText()).toBe('Mary'); + expect(names.get(4).getText()).toBe('John'); + }); + + + *
    + * + * @example + * ### Changing parameters dynamically + * + * All parameters can be changed dynamically. The next example shows how you can make the columns of + * a table sortable, by binding the `expression` and `reverse` parameters to scope properties. + * + + +
    +
    Sort by = {{propertyName}}; reverse = {{reverse}}
    +
    + +
    + + + + + + + + + + + +
    + + + + + + + + +
    {{friend.name}}{{friend.phone}}{{friend.age}}
    +
    +
    + + angular.module('orderByExample2', []) + .controller('ExampleController', ['$scope', function($scope) { + var friends = [ + {name: 'John', phone: '555-1212', age: 10}, + {name: 'Mary', phone: '555-9876', age: 19}, + {name: 'Mike', phone: '555-4321', age: 21}, + {name: 'Adam', phone: '555-5678', age: 35}, + {name: 'Julie', phone: '555-8765', age: 29} + ]; + + $scope.propertyName = 'age'; + $scope.reverse = true; + $scope.friends = friends; + + $scope.sortBy = function(propertyName) { + $scope.reverse = ($scope.propertyName === propertyName) ? !$scope.reverse : false; + $scope.propertyName = propertyName; + }; + }]); + + + .friends { + border-collapse: collapse; + } + + .friends th { + border-bottom: 1px solid; + } + .friends td, .friends th { + border-left: 1px solid; + padding: 5px 10px; + } + .friends td:first-child, .friends th:first-child { + border-left: none; + } + + .sortorder:after { + content: '\25b2'; // BLACK UP-POINTING TRIANGLE + } + .sortorder.reverse:after { + content: '\25bc'; // BLACK DOWN-POINTING TRIANGLE + } + + + // Element locators + var unsortButton = element(by.partialButtonText('unsorted')); + var nameHeader = element(by.partialButtonText('Name')); + var phoneHeader = element(by.partialButtonText('Phone')); + var ageHeader = element(by.partialButtonText('Age')); + var firstName = element(by.repeater('friends').column('friend.name').row(0)); + var lastName = element(by.repeater('friends').column('friend.name').row(4)); + + it('should sort friends by some property, when clicking on the column header', function() { + expect(firstName.getText()).toBe('Adam'); + expect(lastName.getText()).toBe('John'); + + phoneHeader.click(); + expect(firstName.getText()).toBe('John'); + expect(lastName.getText()).toBe('Mary'); + + nameHeader.click(); + expect(firstName.getText()).toBe('Adam'); + expect(lastName.getText()).toBe('Mike'); + + ageHeader.click(); + expect(firstName.getText()).toBe('John'); + expect(lastName.getText()).toBe('Adam'); }); - it('should reorder the table when user selects different predicate', function() { - element('.doc-example-live a:contains("Name")').click(); - expect(repeater('table.friend', 'friend in friends').column('friend.name')). - toEqual(['Adam', 'John', 'Julie', 'Mary', 'Mike']); - expect(repeater('table.friend', 'friend in friends').column('friend.age')). - toEqual(['35', '10', '29', '19', '21']); + it('should sort friends in reverse order, when clicking on the same column', function() { + expect(firstName.getText()).toBe('Adam'); + expect(lastName.getText()).toBe('John'); - element('.doc-example-live a:contains("Phone")').click(); - expect(repeater('table.friend', 'friend in friends').column('friend.phone')). - toEqual(['555-9876', '555-8765', '555-5678', '555-4321', '555-1212']); - expect(repeater('table.friend', 'friend in friends').column('friend.name')). - toEqual(['Mary', 'Julie', 'Adam', 'Mike', 'John']); + ageHeader.click(); + expect(firstName.getText()).toBe('John'); + expect(lastName.getText()).toBe('Adam'); + + ageHeader.click(); + expect(firstName.getText()).toBe('Adam'); + expect(lastName.getText()).toBe('John'); }); -
    -
    + + it('should restore the original order, when clicking "Set to unsorted"', function() { + expect(firstName.getText()).toBe('Adam'); + expect(lastName.getText()).toBe('John'); + + unsortButton.click(); + expect(firstName.getText()).toBe('John'); + expect(lastName.getText()).toBe('Julie'); + }); + + + *
    + * + * @example + * ### Using `orderBy` inside a controller + * + * It is also possible to call the `orderBy` filter manually, by injecting `orderByFilter`, and + * calling it with the desired parameters. (Alternatively, you could inject the `$filter` factory + * and retrieve the `orderBy` filter with `$filter('orderBy')`.) + * + + +
    +
    Sort by = {{propertyName}}; reverse = {{reverse}}
    +
    + +
    + + + + + + + + + + + +
    + + + + + + + + +
    {{friend.name}}{{friend.phone}}{{friend.age}}
    +
    +
    + + angular.module('orderByExample3', []) + .controller('ExampleController', ['$scope', 'orderByFilter', function($scope, orderBy) { + var friends = [ + {name: 'John', phone: '555-1212', age: 10}, + {name: 'Mary', phone: '555-9876', age: 19}, + {name: 'Mike', phone: '555-4321', age: 21}, + {name: 'Adam', phone: '555-5678', age: 35}, + {name: 'Julie', phone: '555-8765', age: 29} + ]; + + $scope.propertyName = 'age'; + $scope.reverse = true; + $scope.friends = orderBy(friends, $scope.propertyName, $scope.reverse); + + $scope.sortBy = function(propertyName) { + $scope.reverse = (propertyName !== null && $scope.propertyName === propertyName) + ? !$scope.reverse : false; + $scope.propertyName = propertyName; + $scope.friends = orderBy(friends, $scope.propertyName, $scope.reverse); + }; + }]); + + + .friends { + border-collapse: collapse; + } + + .friends th { + border-bottom: 1px solid; + } + .friends td, .friends th { + border-left: 1px solid; + padding: 5px 10px; + } + .friends td:first-child, .friends th:first-child { + border-left: none; + } + + .sortorder:after { + content: '\25b2'; // BLACK UP-POINTING TRIANGLE + } + .sortorder.reverse:after { + content: '\25bc'; // BLACK DOWN-POINTING TRIANGLE + } + + + // Element locators + var unsortButton = element(by.partialButtonText('unsorted')); + var nameHeader = element(by.partialButtonText('Name')); + var phoneHeader = element(by.partialButtonText('Phone')); + var ageHeader = element(by.partialButtonText('Age')); + var firstName = element(by.repeater('friends').column('friend.name').row(0)); + var lastName = element(by.repeater('friends').column('friend.name').row(4)); + + it('should sort friends by some property, when clicking on the column header', function() { + expect(firstName.getText()).toBe('Adam'); + expect(lastName.getText()).toBe('John'); + + phoneHeader.click(); + expect(firstName.getText()).toBe('John'); + expect(lastName.getText()).toBe('Mary'); + + nameHeader.click(); + expect(firstName.getText()).toBe('Adam'); + expect(lastName.getText()).toBe('Mike'); + + ageHeader.click(); + expect(firstName.getText()).toBe('John'); + expect(lastName.getText()).toBe('Adam'); + }); + + it('should sort friends in reverse order, when clicking on the same column', function() { + expect(firstName.getText()).toBe('Adam'); + expect(lastName.getText()).toBe('John'); + + ageHeader.click(); + expect(firstName.getText()).toBe('John'); + expect(lastName.getText()).toBe('Adam'); + + ageHeader.click(); + expect(firstName.getText()).toBe('Adam'); + expect(lastName.getText()).toBe('John'); + }); + + it('should restore the original order, when clicking "Set to unsorted"', function() { + expect(firstName.getText()).toBe('Adam'); + expect(lastName.getText()).toBe('John'); + + unsortButton.click(); + expect(firstName.getText()).toBe('John'); + expect(lastName.getText()).toBe('Julie'); + }); + +
    + *
    + * + * @example + * ### Using a custom comparator + * + * If you have very specific requirements about the way items are sorted, you can pass your own + * comparator function. For example, you might need to compare some strings in a locale-sensitive + * way. (When specifying a custom comparator, you also need to pass a value for the `reverse` + * argument - passing `false` retains the default sorting order, i.e. ascending.) + * + + +
    +
    +

    Locale-sensitive Comparator

    + + + + + + + + + +
    NameFavorite Letter
    {{friend.name}}{{friend.favoriteLetter}}
    +
    +
    +

    Default Comparator

    + + + + + + + + + +
    NameFavorite Letter
    {{friend.name}}{{friend.favoriteLetter}}
    +
    +
    +
    + + angular.module('orderByExample4', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.friends = [ + {name: 'John', favoriteLetter: 'Ä'}, + {name: 'Mary', favoriteLetter: 'Ü'}, + {name: 'Mike', favoriteLetter: 'Ö'}, + {name: 'Adam', favoriteLetter: 'H'}, + {name: 'Julie', favoriteLetter: 'Z'} + ]; + + $scope.localeSensitiveComparator = function(v1, v2) { + // If we don't get strings, just compare by index + if (v1.type !== 'string' || v2.type !== 'string') { + return (v1.index < v2.index) ? -1 : 1; + } + + // Compare strings alphabetically, taking locale into account + return v1.value.localeCompare(v2.value); + }; + }]); + + + .friends-container { + display: inline-block; + margin: 0 30px; + } + + .friends { + border-collapse: collapse; + } + + .friends th { + border-bottom: 1px solid; + } + .friends td, .friends th { + border-left: 1px solid; + padding: 5px 10px; + } + .friends td:first-child, .friends th:first-child { + border-left: none; + } + + + // Element locators + var container = element(by.css('.custom-comparator')); + var names = container.all(by.repeater('friends').column('friend.name')); + + it('should sort friends by favorite letter (in correct alphabetical order)', function() { + expect(names.get(0).getText()).toBe('John'); + expect(names.get(1).getText()).toBe('Adam'); + expect(names.get(2).getText()).toBe('Mike'); + expect(names.get(3).getText()).toBe('Mary'); + expect(names.get(4).getText()).toBe('Julie'); + }); + +
    + * */ orderByFilter.$inject = ['$parse']; - function orderByFilter($parse){ - return function(array, sortPredicate, reverseOrder) { - if (!isArray(array)) return array; - if (!sortPredicate) return array; - sortPredicate = isArray(sortPredicate) ? sortPredicate: [sortPredicate]; - sortPredicate = map(sortPredicate, function(predicate){ - var descending = false, get = predicate || identity; - if (isString(predicate)) { - if ((predicate.charAt(0) == '+' || predicate.charAt(0) == '-')) { - descending = predicate.charAt(0) == '-'; - predicate = predicate.substring(1); - } - get = $parse(predicate); - } - return reverseComparator(function(a,b){ - return compare(get(a),get(b)); - }, descending); - }); - var arrayCopy = []; - for ( var i = 0; i < array.length; i++) { arrayCopy.push(array[i]); } - return arrayCopy.sort(reverseComparator(comparator, reverseOrder)); + function orderByFilter($parse) { + return function(array, sortPredicate, reverseOrder, compareFn) { - function comparator(o1, o2){ - for ( var i = 0; i < sortPredicate.length; i++) { - var comp = sortPredicate[i](o1, o2); - if (comp !== 0) return comp; - } - return 0; + if (array == null) return array; + if (!isArrayLike(array)) { + throw minErr('orderBy')('notarray', 'Expected array but received: {0}', array); } - function reverseComparator(comp, descending) { - return toBoolean(descending) - ? function(a,b){return comp(b,a);} - : comp; + + if (!isArray(sortPredicate)) { sortPredicate = [sortPredicate]; } + if (sortPredicate.length === 0) { sortPredicate = ['+']; } + + var predicates = processPredicates(sortPredicate); + + var descending = reverseOrder ? -1 : 1; + + // Define the `compare()` function. Use a default comparator if none is specified. + var compare = isFunction(compareFn) ? compareFn : defaultCompare; + + // The next three lines are a version of a Swartzian Transform idiom from Perl + // (sometimes called the Decorate-Sort-Undecorate idiom) + // See https://en.wikipedia.org/wiki/Schwartzian_transform + var compareValues = Array.prototype.map.call(array, getComparisonObject); + compareValues.sort(doComparison); + array = compareValues.map(function(item) { return item.value; }); + + return array; + + function getComparisonObject(value, index) { + // NOTE: We are adding an extra `tieBreaker` value based on the element's index. + // This will be used to keep the sort stable when none of the input predicates can + // distinguish between two elements. + return { + value: value, + tieBreaker: {value: index, type: 'number', index: index}, + predicateValues: predicates.map(function(predicate) { + return getPredicateValue(predicate.get(value), index); + }) + }; } - function compare(v1, v2){ - var t1 = typeof v1; - var t2 = typeof v2; - if (t1 == t2) { - if (t1 == "string") { - v1 = v1.toLowerCase(); - v2 = v2.toLowerCase(); + + function doComparison(v1, v2) { + for (var i = 0, ii = predicates.length; i < ii; i++) { + var result = compare(v1.predicateValues[i], v2.predicateValues[i]); + if (result) { + return result * predicates[i].descending * descending; } - if (v1 === v2) return 0; - return v1 < v2 ? -1 : 1; - } else { - return t1 < t2 ? -1 : 1; } + + return (compare(v1.tieBreaker, v2.tieBreaker) || defaultCompare(v1.tieBreaker, v2.tieBreaker)) * descending; } }; + + function processPredicates(sortPredicates) { + return sortPredicates.map(function(predicate) { + var descending = 1, get = identity; + + if (isFunction(predicate)) { + get = predicate; + } else if (isString(predicate)) { + if ((predicate.charAt(0) === '+' || predicate.charAt(0) === '-')) { + descending = predicate.charAt(0) === '-' ? -1 : 1; + predicate = predicate.substring(1); + } + if (predicate !== '') { + get = $parse(predicate); + if (get.constant) { + var key = get(); + get = function(value) { return value[key]; }; + } + } + } + return {get: get, descending: descending}; + }); + } + + function isPrimitive(value) { + switch (typeof value) { + case 'number': /* falls through */ + case 'boolean': /* falls through */ + case 'string': + return true; + default: + return false; + } + } + + function objectValue(value) { + // If `valueOf` is a valid function use that + if (isFunction(value.valueOf)) { + value = value.valueOf(); + if (isPrimitive(value)) return value; + } + // If `toString` is a valid function and not the one from `Object.prototype` use that + if (hasCustomToString(value)) { + value = value.toString(); + if (isPrimitive(value)) return value; + } + + return value; + } + + function getPredicateValue(value, index) { + var type = typeof value; + if (value === null) { + type = 'string'; + value = 'null'; + } else if (type === 'object') { + value = objectValue(value); + } + return {value: value, type: type, index: index}; + } + + function defaultCompare(v1, v2) { + var result = 0; + var type1 = v1.type; + var type2 = v2.type; + + if (type1 === type2) { + var value1 = v1.value; + var value2 = v2.value; + + if (type1 === 'string') { + // Compare strings case-insensitively + value1 = value1.toLowerCase(); + value2 = value2.toLowerCase(); + } else if (type1 === 'object') { + // For basic objects, use the position of the object + // in the collection instead of the value + if (isObject(value1)) value1 = v1.index; + if (isObject(value2)) value2 = v2.index; + } + + if (value1 !== value2) { + result = value1 < value2 ? -1 : 1; + } + } else { + result = type1 < type2 ? -1 : 1; + } + + return result; + } } function ngDirective(directive) { @@ -14820,41 +22699,29 @@ /** * @ngdoc directive - * @name ng.directive:a + * @name a * @restrict E * * @description - * Modifies the default behavior of the html A tag so that the default action is prevented when + * Modifies the default behavior of the html a tag so that the default action is prevented when * the href attribute is empty. * - * This change permits the easy creation of action links with the `ngClick` directive - * without changing the location or causing page reloads, e.g.: - * `Add Item` + * For dynamically creating `href` attributes for a tags, see the {@link ng.ngHref `ngHref`} directive. */ var htmlAnchorDirective = valueFn({ restrict: 'E', compile: function(element, attr) { - - if (msie <= 8) { - - // turn link into a stylable link in IE - // but only if it doesn't have name attribute, in which case it's an anchor - if (!attr.href && !attr.name) { - attr.$set('href', ''); - } - - // add a comment node to anchors to workaround IE bug that causes element content to be reset - // to new attribute content if attribute is updated with value containing @ and element also - // contains value with @ - // see issue #1949 - element.append(document.createComment('IE fix')); - } - - if (!attr.href && !attr.name) { + if (!attr.href && !attr.xlinkHref) { return function(scope, element) { - element.on('click', function(event){ + // If the linked element is not an anchor tag anymore, do nothing + if (element[0].nodeName.toLowerCase() !== 'a') return; + + // SVGAElement does not use the href attribute, but rather the 'xlinkHref' attribute. + var href = toString.call(element.prop('href')) === '[object SVGAnimatedString]' ? + 'xlink:href' : 'href'; + element.on('click', function(event) { // if we have no href url, then don't navigate anywhere. - if (!element.attr('href')) { + if (!element.attr(href)) { event.preventDefault(); } }); @@ -14865,7 +22732,7 @@ /** * @ngdoc directive - * @name ng.directive:ngHref + * @name ngHref * @restrict A * @priority 99 * @@ -14874,19 +22741,18 @@ * make the link go to the wrong URL if the user clicks it before * Angular has a chance to replace the `{{hash}}` markup with its * value. Until Angular replaces the markup the link will be broken - * and will most likely return a 404 error. - * - * The `ngHref` directive solves this problem. + * and will most likely return a 404 error. The `ngHref` directive + * solves this problem. * * The wrong way to write it: - *
    -   * 
    -   * 
    + * ```html + * link1 + * ``` * * The correct way to write it: - *
    -   * 
    -   * 
    + * ```html + * link1 + * ``` * * @element A * @param {template} ngHref any string which can contain `{{}}` markup. @@ -14894,8 +22760,8 @@ * @example * This example shows various combinations of `href`, `ng-href` and `ng-click` attributes * in links and their different behaviors: - - + +
    link 1 (link, don't reload)
    link 2 (link, don't reload)
    @@ -14903,53 +22769,69 @@ anchor (link, don't reload)
    anchor (no link)
    link (link, change location) -
    - + + it('should execute ng-click but not reload when href without value', function() { - element('#link-1').click(); - expect(input('value').val()).toEqual('1'); - expect(element('#link-1').attr('href')).toBe(""); + element(by.id('link-1')).click(); + expect(element(by.model('value')).getAttribute('value')).toEqual('1'); + expect(element(by.id('link-1')).getAttribute('href')).toBe(''); }); it('should execute ng-click but not reload when href empty string', function() { - element('#link-2').click(); - expect(input('value').val()).toEqual('2'); - expect(element('#link-2').attr('href')).toBe(""); + element(by.id('link-2')).click(); + expect(element(by.model('value')).getAttribute('value')).toEqual('2'); + expect(element(by.id('link-2')).getAttribute('href')).toBe(''); }); it('should execute ng-click and change url when ng-href specified', function() { - expect(element('#link-3').attr('href')).toBe("/123"); + expect(element(by.id('link-3')).getAttribute('href')).toMatch(/\/123$/); - element('#link-3').click(); - expect(browser().window().path()).toEqual('/123'); + element(by.id('link-3')).click(); + + // At this point, we navigate away from an Angular page, so we need + // to use browser.driver to get the base webdriver. + + browser.wait(function() { + return browser.driver.getCurrentUrl().then(function(url) { + return url.match(/\/123$/); + }); + }, 5000, 'page should navigate to /123'); }); it('should execute ng-click but not reload when href empty string and name specified', function() { - element('#link-4').click(); - expect(input('value').val()).toEqual('4'); - expect(element('#link-4').attr('href')).toBe(''); + element(by.id('link-4')).click(); + expect(element(by.model('value')).getAttribute('value')).toEqual('4'); + expect(element(by.id('link-4')).getAttribute('href')).toBe(''); }); it('should execute ng-click but not reload when no href but name specified', function() { - element('#link-5').click(); - expect(input('value').val()).toEqual('5'); - expect(element('#link-5').attr('href')).toBe(undefined); + element(by.id('link-5')).click(); + expect(element(by.model('value')).getAttribute('value')).toEqual('5'); + expect(element(by.id('link-5')).getAttribute('href')).toBe(null); }); it('should only change url when only ng-href', function() { - input('value').enter('6'); - expect(element('#link-6').attr('href')).toBe('6'); + element(by.model('value')).clear(); + element(by.model('value')).sendKeys('6'); + expect(element(by.id('link-6')).getAttribute('href')).toMatch(/\/6$/); - element('#link-6').click(); - expect(browser().location().url()).toEqual('/6'); + element(by.id('link-6')).click(); + + // At this point, we navigate away from an Angular page, so we need + // to use browser.driver to get the base webdriver. + browser.wait(function() { + return browser.driver.getCurrentUrl().then(function(url) { + return url.match(/\/6$/); + }); + }, 5000, 'page should navigate to /6'); }); - -
    + + */ /** * @ngdoc directive - * @name ng.directive:ngSrc + * @name ngSrc * @restrict A * @priority 99 * @@ -14960,14 +22842,14 @@ * `{{hash}}`. The `ngSrc` directive solves this problem. * * The buggy way to write it: - *
    -   * 
    -   * 
    + * ```html + * Description + * ``` * * The correct way to write it: - *
    -   * 
    -   * 
    + * ```html + * Description + * ``` * * @element IMG * @param {template} ngSrc any string which can contain `{{}}` markup. @@ -14975,7 +22857,7 @@ /** * @ngdoc directive - * @name ng.directive:ngSrcset + * @name ngSrcset * @restrict A * @priority 99 * @@ -14986,14 +22868,14 @@ * `{{hash}}`. The `ngSrcset` directive solves this problem. * * The buggy way to write it: - *
    -   * 
    -   * 
    + * ```html + * Description + * ``` * * The correct way to write it: - *
    -   * 
    -   * 
    + * ```html + * Description + * ``` * * @element IMG * @param {template} ngSrcset any string which can contain `{{}}` markup. @@ -15001,111 +22883,105 @@ /** * @ngdoc directive - * @name ng.directive:ngDisabled + * @name ngDisabled * @restrict A * @priority 100 * * @description * - * The following markup will make the button enabled on Chrome/Firefox but not on IE8 and older IEs: - *
    -   * 
    - * - *
    - *
    + * This directive sets the `disabled` attribute on the element (typically a form control, + * e.g. `input`, `button`, `select` etc.) if the + * {@link guide/expression expression} inside `ngDisabled` evaluates to truthy. * - * The HTML specification does not require browsers to preserve the values of boolean attributes - * such as disabled. (Their presence means true and their absence means false.) - * If we put an Angular interpolation expression into such an attribute then the - * binding information would be lost when the browser removes the attribute. - * The `ngDisabled` directive solves this problem for the `disabled` attribute. - * This complementary directive is not removed by the browser and so provides - * a permanent reliable place to store the binding information. + * A special directive is necessary because we cannot use interpolation inside the `disabled` + * attribute. See the {@link guide/interpolation interpolation guide} for more info. * * @example - - - Click me to toggle:
    + + +
    -
    - + + it('should toggle button', function() { - expect(element('.doc-example-live :button').prop('disabled')).toBeFalsy(); - input('checked').check(); - expect(element('.doc-example-live :button').prop('disabled')).toBeTruthy(); + expect(element(by.css('button')).getAttribute('disabled')).toBeFalsy(); + element(by.model('checked')).click(); + expect(element(by.css('button')).getAttribute('disabled')).toBeTruthy(); }); - -
    + + * * @element INPUT * @param {expression} ngDisabled If the {@link guide/expression expression} is truthy, - * then special attribute "disabled" will be set on the element + * then the `disabled` attribute will be set on the element */ /** * @ngdoc directive - * @name ng.directive:ngChecked + * @name ngChecked * @restrict A * @priority 100 * * @description - * The HTML specification does not require browsers to preserve the values of boolean attributes - * such as checked. (Their presence means true and their absence means false.) - * If we put an Angular interpolation expression into such an attribute then the - * binding information would be lost when the browser removes the attribute. - * The `ngChecked` directive solves this problem for the `checked` attribute. - * This complementary directive is not removed by the browser and so provides - * a permanent reliable place to store the binding information. + * Sets the `checked` attribute on the element, if the expression inside `ngChecked` is truthy. + * + * Note that this directive should not be used together with {@link ngModel `ngModel`}, + * as this can lead to unexpected behavior. + * + * A special directive is necessary because we cannot use interpolation inside the `checked` + * attribute. See the {@link guide/interpolation interpolation guide} for more info. + * * @example - - - Check me to check both:
    - -
    - + + +
    + +
    + it('should check both checkBoxes', function() { - expect(element('.doc-example-live #checkSlave').prop('checked')).toBeFalsy(); - input('master').check(); - expect(element('.doc-example-live #checkSlave').prop('checked')).toBeTruthy(); + expect(element(by.id('checkSlave')).getAttribute('checked')).toBeFalsy(); + element(by.model('master')).click(); + expect(element(by.id('checkSlave')).getAttribute('checked')).toBeTruthy(); }); -
    -
    + + * * @element INPUT * @param {expression} ngChecked If the {@link guide/expression expression} is truthy, - * then special attribute "checked" will be set on the element + * then the `checked` attribute will be set on the element */ /** * @ngdoc directive - * @name ng.directive:ngReadonly + * @name ngReadonly * @restrict A * @priority 100 * * @description - * The HTML specification does not require browsers to preserve the values of boolean attributes - * such as readonly. (Their presence means true and their absence means false.) - * If we put an Angular interpolation expression into such an attribute then the - * binding information would be lost when the browser removes the attribute. - * The `ngReadonly` directive solves this problem for the `readonly` attribute. - * This complementary directive is not removed by the browser and so provides - * a permanent reliable place to store the binding information. + * + * Sets the `readonly` attribute on the element, if the expression inside `ngReadonly` is truthy. + * Note that `readonly` applies only to `input` elements with specific types. [See the input docs on + * MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-readonly) for more information. + * + * A special directive is necessary because we cannot use interpolation inside the `readonly` + * attribute. See the {@link guide/interpolation interpolation guide} for more info. + * * @example - - - Check me to make text readonly:
    - -
    - + + +
    + +
    + it('should toggle readonly attr', function() { - expect(element('.doc-example-live :text').prop('readonly')).toBeFalsy(); - input('checked').check(); - expect(element('.doc-example-live :text').prop('readonly')).toBeTruthy(); + expect(element(by.css('[type="text"]')).getAttribute('readonly')).toBeFalsy(); + element(by.model('checked')).click(); + expect(element(by.css('[type="text"]')).getAttribute('readonly')).toBeTruthy(); }); -
    -
    + + * * @element INPUT * @param {expression} ngReadonly If the {@link guide/expression expression} is truthy, @@ -15115,36 +22991,41 @@ /** * @ngdoc directive - * @name ng.directive:ngSelected + * @name ngSelected * @restrict A * @priority 100 * * @description - * The HTML specification does not require browsers to preserve the values of boolean attributes - * such as selected. (Their presence means true and their absence means false.) - * If we put an Angular interpolation expression into such an attribute then the - * binding information would be lost when the browser removes the attribute. - * The `ngSelected` directive solves this problem for the `selected` atttribute. - * This complementary directive is not removed by the browser and so provides - * a permanent reliable place to store the binding information. + * + * Sets the `selected` attribute on the element, if the expression inside `ngSelected` is truthy. + * + * A special directive is necessary because we cannot use interpolation inside the `selected` + * attribute. See the {@link guide/interpolation interpolation guide} for more info. + * + *
    + * **Note:** `ngSelected` does not interact with the `select` and `ngModel` directives, it only + * sets the `selected` attribute on the element. If you are using `ngModel` on the select, you + * should not use `ngSelected` on the options, as `ngModel` will set the select value and + * selected options. + *
    * * @example - - - Check me to select:
    -
    + -
    - + + it('should select Greetings!', function() { - expect(element('.doc-example-live #greet').prop('selected')).toBeFalsy(); - input('selected').check(); - expect(element('.doc-example-live #greet').prop('selected')).toBeTruthy(); + expect(element(by.id('greet')).getAttribute('selected')).toBeFalsy(); + element(by.model('selected')).click(); + expect(element(by.id('greet')).getAttribute('selected')).toBeTruthy(); }); - -
    + + * * @element OPTION * @param {expression} ngSelected If the {@link guide/expression expression} is truthy, @@ -15153,34 +23034,43 @@ /** * @ngdoc directive - * @name ng.directive:ngOpen + * @name ngOpen * @restrict A * @priority 100 * * @description - * The HTML specification does not require browsers to preserve the values of boolean attributes - * such as open. (Their presence means true and their absence means false.) - * If we put an Angular interpolation expression into such an attribute then the - * binding information would be lost when the browser removes the attribute. - * The `ngOpen` directive solves this problem for the `open` attribute. - * This complementary directive is not removed by the browser and so provides - * a permanent reliable place to store the binding information. + * + * Sets the `open` attribute on the element, if the expression inside `ngOpen` is truthy. + * + * A special directive is necessary because we cannot use interpolation inside the `open` + * attribute. See the {@link guide/interpolation interpolation guide} for more info. + * + * ## A note about browser compatibility + * + * Internet Explorer and Edge do not support the `details` element, it is + * recommended to use {@link ng.ngShow} and {@link ng.ngHide} instead. + * * @example - - - Check me check multiple:
    + + +
    - Show/Hide me + List +
      +
    • Apple
    • +
    • Orange
    • +
    • Durian
    • +
    -
    - + + it('should toggle open', function() { - expect(element('#details').prop('open')).toBeFalsy(); - input('open').check(); - expect(element('#details').prop('open')).toBeTruthy(); + expect(element(by.id('details')).getAttribute('open')).toBeFalsy(); + element(by.model('open')).click(); + expect(element(by.id('details')).getAttribute('open')).toBeTruthy(); }); - -
    + + * * @element DETAILS * @param {expression} ngOpen If the {@link guide/expression expression} is truthy, @@ -15189,26 +23079,62 @@ var ngAttributeAliasDirectives = {}; - // boolean attrs are evaluated forEach(BOOLEAN_ATTR, function(propName, attrName) { // binding to multiple is not supported - if (propName == "multiple") return; + if (propName === 'multiple') return; + + function defaultLinkFn(scope, element, attr) { + scope.$watch(attr[normalized], function ngBooleanAttrWatchAction(value) { + attr.$set(attrName, !!value); + }); + } var normalized = directiveNormalize('ng-' + attrName); + var linkFn = defaultLinkFn; + + if (propName === 'checked') { + linkFn = function(scope, element, attr) { + // ensuring ngChecked doesn't interfere with ngModel when both are set on the same input + if (attr.ngModel !== attr[normalized]) { + defaultLinkFn(scope, element, attr); + } + }; + } + ngAttributeAliasDirectives[normalized] = function() { + return { + restrict: 'A', + priority: 100, + link: linkFn + }; + }; + }); + +// aliased input attrs are evaluated + forEach(ALIASED_ATTR, function(htmlAttr, ngAttr) { + ngAttributeAliasDirectives[ngAttr] = function() { return { priority: 100, link: function(scope, element, attr) { - scope.$watch(attr[normalized], function ngBooleanAttrWatchAction(value) { - attr.$set(attrName, !!value); + //special case ngPattern when a literal regular expression value + //is used as the expression (this way we don't have to watch anything). + if (ngAttr === 'ngPattern' && attr.ngPattern.charAt(0) === '/') { + var match = attr.ngPattern.match(REGEX_STRING_REGEXP); + if (match) { + attr.$set('ngPattern', new RegExp(match[1], match[2])); + return; + } + } + + scope.$watch(attr[ngAttr], function ngAttrAliasWatchAction(value) { + attr.$set(ngAttr, value); }); } }; }; }); - // ng-src, ng-srcset, ng-href are interpolated forEach(['src', 'srcset', 'href'], function(attrName) { var normalized = directiveNormalize('ng-' + attrName); @@ -15216,50 +23142,81 @@ return { priority: 99, // it needs to run after the attributes are interpolated link: function(scope, element, attr) { + var propName = attrName, + name = attrName; + + if (attrName === 'href' && + toString.call(element.prop('href')) === '[object SVGAnimatedString]') { + name = 'xlinkHref'; + attr.$attr[name] = 'xlink:href'; + propName = null; + } + attr.$observe(normalized, function(value) { - if (!value) + if (!value) { + if (attrName === 'href') { + attr.$set(name, null); + } return; + } - attr.$set(attrName, value); + attr.$set(name, value); - // on IE, if "ng:src" directive declaration is used and "src" attribute doesn't exist + // Support: IE 9-11 only + // On IE, if "ng:src" directive declaration is used and "src" attribute doesn't exist // then calling element.setAttribute('src', 'foo') doesn't do anything, so we need // to set the property as well to achieve the desired effect. - // we use attr[attrName] value since $set can sanitize the url. - if (msie) element.prop(attrName, attr[attrName]); + // We use attr[attrName] value since $set can sanitize the url. + if (msie && propName) element.prop(propName, attr[name]); }); } }; }; }); - /* global -nullFormCtrl */ + /* global -nullFormCtrl, -PENDING_CLASS, -SUBMITTED_CLASS + */ var nullFormCtrl = { - $addControl: noop, - $removeControl: noop, - $setValidity: noop, - $setDirty: noop, - $setPristine: noop - }; + $addControl: noop, + $$renameControl: nullFormRenameControl, + $removeControl: noop, + $setValidity: noop, + $setDirty: noop, + $setPristine: noop, + $setSubmitted: noop + }, + PENDING_CLASS = 'ng-pending', + SUBMITTED_CLASS = 'ng-submitted'; + + function nullFormRenameControl(control, name) { + control.$name = name; + } /** - * @ngdoc object - * @name ng.directive:form.FormController + * @ngdoc type + * @name form.FormController * * @property {boolean} $pristine True if user has not interacted with the form yet. * @property {boolean} $dirty True if user has already interacted with the form. * @property {boolean} $valid True if all of the containing forms and controls are valid. * @property {boolean} $invalid True if at least one containing control or form is invalid. + * @property {boolean} $submitted True if user has submitted the form even if its invalid. * - * @property {Object} $error Is an object hash, containing references to all invalid controls or - * forms, where: + * @property {Object} $pending An object hash, containing references to controls or forms with + * pending validators, where: + * + * - keys are validations tokens (error names). + * - values are arrays of controls or forms that have a pending validator for the given error name. + * + * See {@link form.FormController#$error $error} for a list of built-in validation tokens. + * + * @property {Object} $error An object hash, containing references to controls or forms with failing + * validators, where: * * - keys are validation tokens (error names), - * - values are arrays of controls or forms that are invalid for given error name. - * + * - values are arrays of controls or forms that have a failing validator for the given error name. * * Built-in validation tokens: - * * - `email` * - `max` * - `maxlength` @@ -15269,9 +23226,14 @@ * - `pattern` * - `required` * - `url` + * - `date` + * - `datetimelocal` + * - `time` + * - `week` + * - `month` * * @description - * `FormController` keeps track of all its controls and nested forms as well as state of them, + * `FormController` keeps track of all its controls and nested forms as well as the state of them, * such as being valid/invalid or dirty/pristine. * * Each {@link ng.directive:form form} directive creates an instance @@ -15279,129 +23241,148 @@ * */ //asks for $scope to fool the BC controller module - FormController.$inject = ['$element', '$attrs', '$scope']; - function FormController(element, attrs) { - var form = this, - parentForm = element.parent().controller('form') || nullFormCtrl, - invalidCount = 0, // used to easily determine if we are valid - errors = form.$error = {}, - controls = []; + FormController.$inject = ['$element', '$attrs', '$scope', '$animate', '$interpolate']; + function FormController($element, $attrs, $scope, $animate, $interpolate) { + this.$$controls = []; // init state - form.$name = attrs.name || attrs.ngForm; - form.$dirty = false; - form.$pristine = true; - form.$valid = true; - form.$invalid = false; + this.$error = {}; + this.$$success = {}; + this.$pending = undefined; + this.$name = $interpolate($attrs.name || $attrs.ngForm || '')($scope); + this.$dirty = false; + this.$pristine = true; + this.$valid = true; + this.$invalid = false; + this.$submitted = false; + this.$$parentForm = nullFormCtrl; - parentForm.$addControl(form); + this.$$element = $element; + this.$$animate = $animate; - // 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); - } + setupValidity(this); + } + FormController.prototype = { /** - * @ngdoc function - * @name ng.directive:form.FormController#$addControl - * @methodOf ng.directive:form.FormController + * @ngdoc method + * @name form.FormController#$rollbackViewValue * * @description - * Register a control with the form. + * Rollback all form controls pending updates to the `$modelValue`. * - * Input elements using ngModelController do this automatically when they are linked. + * 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 typically needed by the reset button of + * a form that uses `ng-model-options` to pend updates. */ - form.$addControl = function(control) { + $rollbackViewValue: function() { + forEach(this.$$controls, function(control) { + control.$rollbackViewValue(); + }); + }, + + /** + * @ngdoc method + * @name form.FormController#$commitViewValue + * + * @description + * Commit all form controls pending updates 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() { + forEach(this.$$controls, function(control) { + control.$commitViewValue(); + }); + }, + + /** + * @ngdoc method + * @name form.FormController#$addControl + * @param {object} control control object, either a {@link form.FormController} or an + * {@link ngModel.NgModelController} + * + * @description + * Register a control with the form. Input elements using ngModelController do this automatically + * when they are linked. + * + * Note that the current state of the control will not be reflected on the new parent form. This + * is not an issue with normal use, as freshly compiled and linked controls are in a `$pristine` + * state. + * + * However, if the method is used programmatically, for example by adding dynamically created controls, + * or controls that have been previously removed without destroying their corresponding DOM element, + * it's the developers responsibility to make sure the current state propagates to the parent form. + * + * For example, if an input control is added that is already `$dirty` and has `$error` properties, + * calling `$setDirty()` and `$validate()` afterwards will propagate the state to the parent form. + */ + $addControl: function(control) { // Breaking change - before, inputs whose name was "hasOwnProperty" were quietly ignored // and not added to the scope. Now we throw an error. assertNotHasOwnProperty(control.$name, 'input'); - controls.push(control); + this.$$controls.push(control); if (control.$name) { - form[control.$name] = control; + this[control.$name] = control; } - }; + + control.$$parentForm = this; + }, + + // Private API: rename a form control + $$renameControl: function(control, newName) { + var oldName = control.$name; + + if (this[oldName] === control) { + delete this[oldName]; + } + this[newName] = control; + control.$name = newName; + }, /** - * @ngdoc function - * @name ng.directive:form.FormController#$removeControl - * @methodOf ng.directive:form.FormController + * @ngdoc method + * @name form.FormController#$removeControl + * @param {object} control control object, either a {@link form.FormController} or an + * {@link ngModel.NgModelController} * * @description * Deregister a control from the form. * * Input elements using ngModelController do this automatically when they are destroyed. + * + * Note that only the removed control's validation state (`$errors`etc.) will be removed from the + * form. `$dirty`, `$submitted` states will not be changed, because the expected behavior can be + * different from case to case. For example, removing the only `$dirty` control from a form may or + * may not mean that the form is still `$dirty`. */ - form.$removeControl = function(control) { - if (control.$name && form[control.$name] === control) { - delete form[control.$name]; + $removeControl: function(control) { + if (control.$name && this[control.$name] === control) { + delete this[control.$name]; } - forEach(errors, function(queue, validationToken) { - form.$setValidity(validationToken, true, control); - }); + forEach(this.$pending, function(value, name) { + // eslint-disable-next-line no-invalid-this + this.$setValidity(name, null, control); + }, this); + forEach(this.$error, function(value, name) { + // eslint-disable-next-line no-invalid-this + this.$setValidity(name, null, control); + }, this); + forEach(this.$$success, function(value, name) { + // eslint-disable-next-line no-invalid-this + this.$setValidity(name, null, control); + }, this); - arrayRemove(controls, control); - }; + arrayRemove(this.$$controls, control); + control.$$parentForm = nullFormCtrl; + }, /** - * @ngdoc function - * @name ng.directive:form.FormController#$setValidity - * @methodOf ng.directive:form.FormController - * - * @description - * Sets the validity of a form control. - * - * This method will also propagate to parent forms. - */ - form.$setValidity = function(validationToken, isValid, control) { - var queue = errors[validationToken]; - - if (isValid) { - if (queue) { - arrayRemove(queue, control); - if (!queue.length) { - invalidCount--; - if (!invalidCount) { - toggleValidCss(isValid); - form.$valid = true; - form.$invalid = false; - } - errors[validationToken] = false; - toggleValidCss(true, validationToken); - parentForm.$setValidity(validationToken, true, form); - } - } - - } else { - if (!invalidCount) { - toggleValidCss(isValid); - } - if (queue) { - if (includes(queue, control)) return; - } else { - errors[validationToken] = queue = []; - invalidCount++; - toggleValidCss(false, validationToken); - parentForm.$setValidity(validationToken, false, form); - } - queue.push(control); - - form.$valid = false; - form.$invalid = true; - } - }; - - /** - * @ngdoc function - * @name ng.directive:form.FormController#$setDirty - * @methodOf ng.directive:form.FormController + * @ngdoc method + * @name form.FormController#$setDirty * * @description * Sets the form to a dirty state. @@ -15409,42 +23390,125 @@ * This method can be called to add the 'ng-dirty' class and set the form to a dirty * state (ng-dirty class). This method will also propagate to parent forms. */ - form.$setDirty = function() { - element.removeClass(PRISTINE_CLASS).addClass(DIRTY_CLASS); - form.$dirty = true; - form.$pristine = false; - parentForm.$setDirty(); - }; + $setDirty: function() { + this.$$animate.removeClass(this.$$element, PRISTINE_CLASS); + this.$$animate.addClass(this.$$element, DIRTY_CLASS); + this.$dirty = true; + this.$pristine = false; + this.$$parentForm.$setDirty(); + }, /** - * @ngdoc function - * @name ng.directive:form.FormController#$setPristine - * @methodOf ng.directive:form.FormController + * @ngdoc method + * @name form.FormController#$setPristine * * @description * Sets the form to its pristine state. * - * This method can be called to remove the 'ng-dirty' class and set the form to its pristine - * state (ng-pristine class). This method will also propagate to all the controls contained - * in this form. + * This method sets the form's `$pristine` state to true, the `$dirty` state to false, removes + * the `ng-dirty` class and adds the `ng-pristine` class. Additionally, it sets the `$submitted` + * state to false. + * + * This method will also propagate to all the controls contained in this form. * * Setting a form back to a pristine state is often useful when we want to 'reuse' a form after * saving or resetting it. */ - form.$setPristine = function () { - element.removeClass(DIRTY_CLASS).addClass(PRISTINE_CLASS); - form.$dirty = false; - form.$pristine = true; - forEach(controls, function(control) { + $setPristine: function() { + this.$$animate.setClass(this.$$element, PRISTINE_CLASS, DIRTY_CLASS + ' ' + SUBMITTED_CLASS); + this.$dirty = false; + this.$pristine = true; + this.$submitted = false; + forEach(this.$$controls, function(control) { control.$setPristine(); }); - }; - } + }, + /** + * @ngdoc method + * @name form.FormController#$setUntouched + * + * @description + * Sets the form to its untouched state. + * + * This method can be called to remove the 'ng-touched' class and set the form controls to their + * untouched state (ng-untouched class). + * + * Setting a form controls back to their untouched state is often useful when setting the form + * back to its pristine state. + */ + $setUntouched: function() { + forEach(this.$$controls, function(control) { + control.$setUntouched(); + }); + }, + + /** + * @ngdoc method + * @name form.FormController#$setSubmitted + * + * @description + * Sets the form to its submitted state. + */ + $setSubmitted: function() { + this.$$animate.addClass(this.$$element, SUBMITTED_CLASS); + this.$submitted = true; + this.$$parentForm.$setSubmitted(); + } + }; + + /** + * @ngdoc method + * @name form.FormController#$setValidity + * + * @description + * Change the validity state of the form, and notify the parent form (if any). + * + * Application developers will rarely need to call this method directly. It is used internally, by + * {@link ngModel.NgModelController#$setValidity NgModelController.$setValidity()}, to propagate a + * control's validity state to the parent `FormController`. + * + * @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.$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 AngularJS when validators do not run because of parse errors and when + * `$asyncValidators` do not run because any of the `$validators` failed. + * @param {NgModelController | FormController} controller - The controller whose validity state is + * triggering the change. + */ + addSetValidityMethod({ + clazz: FormController, + set: function(object, property, controller) { + var list = object[property]; + if (!list) { + object[property] = [controller]; + } else { + var index = list.indexOf(controller); + if (index === -1) { + list.push(controller); + } + } + }, + unset: function(object, property, controller) { + var list = object[property]; + if (!list) { + return; + } + arrayRemove(list, controller); + if (list.length === 0) { + delete object[property]; + } + } + }); /** * @ngdoc directive - * @name ng.directive:ngForm + * @name ngForm * @restrict EAC * * @description @@ -15452,6 +23516,10 @@ * does not allow nesting of form elements. It is useful to nest forms, for example if the validity of a * sub-group of controls needs to be determined. * + * Note: the purpose of `ngForm` is to group controls, + * but not to be a replacement for the `
    ` tag with all of its capabilities + * (e.g. posting to the server, ...). + * * @param {string=} ngForm|name Name of the form. If specified, the form controller will be published into * related scope, under this name. * @@ -15459,33 +23527,33 @@ /** * @ngdoc directive - * @name ng.directive:form + * @name form * @restrict E * * @description * Directive that instantiates - * {@link ng.directive:form.FormController FormController}. + * {@link form.FormController FormController}. * * If the `name` attribute is specified, the form controller is published onto the current scope under * this name. * * # Alias: {@link ng.directive:ngForm `ngForm`} * - * In Angular forms can be nested. This means that the outer form is valid when all of the child + * In Angular, forms can be nested. This means that the outer form is valid when all of the child * forms are valid as well. However, browsers do not allow nesting of `` elements, so - * Angular provides the {@link ng.directive:ngForm `ngForm`} directive which behaves identically to - * `` but can be nested. This allows you to have nested forms, which is very useful when - * using Angular validation directives in forms that are dynamically generated using the - * {@link ng.directive:ngRepeat `ngRepeat`} directive. Since you cannot dynamically generate the `name` - * attribute of input elements using interpolation, you have to wrap each set of repeated inputs in an - * `ngForm` directive and nest these in an outer `form` element. - * + * Angular provides the {@link ng.directive:ngForm `ngForm`} directive, which behaves identically to + * `form` but can be nested. Nested forms can be useful, for example, if the validity of a sub-group + * of controls needs to be determined. * * # CSS classes * - `ng-valid` is set if the form is valid. * - `ng-invalid` is set if the form is invalid. + * - `ng-pending` is set if the form is pending. * - `ng-pristine` is set if the form is pristine. * - `ng-dirty` is set if the form is dirty. + * - `ng-submitted` is set if the form was submitted. + * + * Keep in mind that ngAnimate can detect each of these classes when added and removed. * * * # Submitting a form and preventing the default action @@ -15517,121 +23585,325 @@ * hitting enter in any of the input fields will trigger the click handler on the *first* button or * input[type=submit] (`ngClick`) *and* a submit handler on the enclosing form (`ngSubmit`) * - * @param {string=} name Name of the form. If specified, the form controller will be published into - * related scope, under this name. + * Any pending `ngModelOptions` changes will take place immediately when an enclosing form is + * submitted. Note that `ngClick` events will occur before the model is updated. Use `ngSubmit` + * to have access to the updated model. + * + * ## Animation Hooks + * + * Animations in ngForm are triggered when any of the associated CSS classes are added and removed. + * These classes are: `.ng-pristine`, `.ng-dirty`, `.ng-invalid` and `.ng-valid` as well as any + * other validations that are performed within the form. Animations in ngForm are similar to how + * they work in ngClass and animations can be hooked into using CSS transitions, keyframes as well + * as JS animations. + * + * The following example shows a simple way to utilize CSS transitions to style a form element + * that has been rendered as invalid after it has been validated: + * + *
    +   * //be sure to include ngAnimate as a module to hook into more
    +   * //advanced animations
    +   * .my-form {
    + *   transition:0.5s linear all;
    + *   background: white;
    + * }
    +   * .my-form.ng-invalid {
    + *   background: red;
    + *   color:white;
    + * }
    +   * 
    * * @example - - + + - + + userType: Required!
    - userType = {{userType}}
    - 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(); - }); - } - - if (attrs.nextText) { - attrs.$observe('nextText', function(value) { - nextText = paginationCtrl.getAttributeValue(value, config.nextText, true); - paginationCtrl.render(); - }); - } - - if (attrs.previousText) { - attrs.$observe('previousText', function(value) { - previousText = paginationCtrl.getAttributeValue(value, config.previousText, true); - 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(); @@ -2119,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; @@ -3655,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.view.not_found' | translate }} diff --git a/docs-web/src/main/webapp/src/style/main.less b/docs-web/src/main/webapp/src/style/main.less index 7a27646f..5aac89ee 100644 --- a/docs-web/src/main/webapp/src/style/main.less +++ b/docs-web/src/main/webapp/src/style/main.less @@ -422,6 +422,18 @@ input[readonly].share-link { } } +// PDF viewer +.pdf-viewer { + display: block; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + width: 100%; + height: 80vh; +} + // Vertical alignment .vertical-center { min-height: 100vh; @@ -618,4 +630,12 @@ input[readonly].share-link { .widgets .col-md-12 { padding-left: 0; padding-right: 0; +} + +// Button placeholder +.btn-placeholder { + visibility: hidden; + width: 0; + padding-left: 0; + padding-right: 0; } \ No newline at end of file From 5cee20163dc9f3cfe3c97641e111d024ec2f4bf0 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Fri, 9 Mar 2018 16:44:50 +0100 Subject: [PATCH 170/288] update German translation --- docs-web/src/main/webapp/src/locale/de.json | 62 ++++++++++----------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/docs-web/src/main/webapp/src/locale/de.json b/docs-web/src/main/webapp/src/locale/de.json index 523f4055..e0da9843 100644 --- a/docs-web/src/main/webapp/src/locale/de.json +++ b/docs-web/src/main/webapp/src/locale/de.json @@ -70,7 +70,7 @@ "format": "Format", "source": "Quelle", "type": "Typ", - "Geltungsbereich": "Geltungsbereich", + "coverage": "Geltungsbereich", "rights": "Rechte", "relations": "Beziehung", "page_size": "Seiten Größe", @@ -78,7 +78,7 @@ "page_size_20": "20 pro Seite", "page_size_30": "30 pro Seite", "upgrade_quota": "Fragen Sie Ihren Administrator, um Ihr Speicherplatz zu erweitern.", - "quota": "{{ current | number: 0 }}MB ({{ percent | number: 1 }}%) verwendet {{ total | number: 0 }}MB", + "quota": "{{ current | number: 0 }}MB ({{ percent | number: 1 }}%) verwendet von {{ total | number: 0 }}MB", "count": "{{ count }} Dokument{{ count > 1 ? 's' : '' }} gefunden", "view": { "delete_comment_title": "Kommentar löschen", @@ -164,7 +164,7 @@ "add_files": "Dateien hinzufügen", "add_new_document": "Neues Dokument hinzufügen", "latest_activity": "Letzte Aktivitäten", - "footer_sismics": "Programmiert mit by Sismics", + "footer_sismics": "Programmiert mit by Sismics", "api_documentation": "API Dokumentation", "feedback": "Geben Sie uns Feedback", "workflow_document_list": "Mir zugeordnette Dokumente" @@ -253,7 +253,7 @@ "menu_inbox": "Inbox scannen", "menu_server_logs": "Server Logs", "user": { - "title": "Users management", + "title": "Benutzerverwaltung", "add_user": "Benutzer hinzufügen", "username": "Benutzername", "create_date": "Erstellungsdatum", @@ -271,7 +271,7 @@ "storage_quota": "Speicherkontingent", "storage_quota_placeholder": "Speicherkontingent (in MB)", "password": "Passwort", - "password_confirm": "Passwort (confirm)", + "password_confirm": "Passwort (bestätigen)", "disabled": "Deaktivierter Benutzer", "password_reset_btn": "Senden Sie eine E-Mail zum Zurücksetzen des Kennworts an diesen Benutzer", "password_lost_sent_title": "Passwort zurücksetzen Email gesendet ", @@ -279,35 +279,35 @@ } }, "workflow": { - "title": "Workflow configuration", + "title": "Workflow Konfigurator", "add_workflow": "Workflow hinzufügen", "name": "Name", "create_date": "Erstellungsdatum", "edit": { "delete_workflow_title": "Workflow löschen", "delete_workflow_message": "Möchten Sie diesen Workflow wirklich löschen? Derzeit ausgeführte Workflows werden nicht gelöscht", - "edit_workflow_title": "Edit \"{{ name }}\"", + "edit_workflow_title": "Edit \"{{ name }}\"", "add_workflow_title": "Add a Workflow", "name": "Name", "name_placeholder": "Name des Bearbeitungschritts oder der Beschreibung ", "drag_help": "Drag & drop, um die Schritte neu zu ordnen ", - "type": "Bearbeitungsschritts", + "type": "Bearbeitungsschritt", "type_approve": "Genehmigen", "type_validate": "Bestätigen", "target": "Zugewiesen an", - "target_help": "Approve: Überprüfen und Fortsetzen des Workflows
            Validate: Übernehmen oder lehnen Sie die Überprüfung ab", + "target_help": "Zulassen: Überprüfen und fortsetzen des Workflows
            Genemigen: Übernehmen oder lehnen Sie die Überprüfung ab", "add_step": "Workflow Schritt hinzufügen" } }, "security": { "enable_totp": "Zwei-Faktor-Authentifizierung aktivieren", "enable_totp_message": "Stellen Sie sicher, dass Sie eine TOTP-kompatible Anwendung auf Ihrem Handy haben, die bereit ist, ein neues Konto hinzuzufügen.", - "title": "Two-factor authentication", - "message_1": "Die Zwei-Faktor-Authentifizierung ermöglicht Ihnen eine weitere Abischerung Ihres {{ appName }} Benutzerkonto. Bevor Sie diese Funktion aktivieren, stellen Sie sicher, dass Sie eine TOTP-kompatible Anwendung auf Ihrem Telefon haben: ", + "title": "Zwei-Faktor-Authentifizierung", + "message_1": "Die Zwei-Faktor-Authentifizierung ermöglicht Ihnen eine weitere Abischerung Ihres {{ appName }} Benutzerkontos. Bevor Sie diese Funktion aktivieren, stellen Sie sicher, dass Sie eine TOTP-kompatible Anwendung auf Ihrem Telefon haben: ", "message_google_authenticator": "Für Android, iOS, und Blackberry: Google Authenticator", "message_duo_mobile": "Für Android und iOS: Duo Mobile", "message_authenticator": "Für Windows Phone: Authenticator", - "message_2": "Diese Anwendungen generieren automatisch einen Validierungscode, der sich nach einer gewissen Zeitspanne ändert. Sie müssen diesen Validierungscode jedes Mal eingeben, wenn Sie sich bei {{ appName }}} anmelden. .", + "message_2": "Diese Anwendungen generieren automatisch einen Validierungscod der sich nach einer gewissen Zeitspanne ändert. Sie müssen diesen Validierungscode jedes Mal eingeben, wenn Sie sich bei {{ appName }} anmelden. .", "secret_key": "Ihr geheimer Schlüssel ist: {{ secret }}", "secret_key_warning": "Konfigurieren Sie Ihre TOTP-App jetzt mit diesem geheimen Schlüssel auf Ihrem Telefon, Sie können später nicht darauf zugreifen.", "totp_enabled_message": "Die Zwei-Faktor-Authentifizierung ist in Ihrem Konto aktiviert.
            Bei jeder Anmeldung auf {{ appName }}, werden Sie in Ihrer konfigurierten Telefon-App nach einem Bestätigungscode gefragt.
            Wenn Sie Ihr Telefon verlieren, können Sie sich nicht in Ihrem Konto anmelden, aber aktive Sitzungen ermöglichen es Ihnen, einen geheimen Schlüssel neu zu generieren.", @@ -319,7 +319,7 @@ } }, "group": { - "title": "Gruppen management", + "title": "Gruppenverwaltung", "add_group": "Gruppe hinzufügen", "name": "Name", "edit": { @@ -332,20 +332,20 @@ "name": "Name", "parent_group": "Übergruppe", "search_group": "Gruppe suchen", - "members": "Mitgleider", - "new_member": "Neue Mitgleider", + "members": "Mitglieder", + "new_member": "Neue Mitglieder", "search_user": "Benutzer suchen" } }, "account": { - "title": "User account", + "title": "Benutzerkonto", "password": "Passwort", - "password_confirm": "Passwort (confirm)", + "password_confirm": "Passwort (bestätigen)", "updated": "Benutzerkonto erfolgreich aktualisiert" }, "config": { - "title_guest_access": "Gast access", - "message_guest_access": "Der Gastzugang ist ein Modus, in dem jeder Zugriff hat. {{ appName }} ohne Passwort.
            Wie ein normaler Benutzer kann der Gastbenutzer nur auf seine Dokumente zugreifen und auf die über Berechtigungen zugreifen.
            ", + "title_guest_access": "Gastzugang", + "message_guest_access": "Der Gastzugang ist ein Modus in dem jeder Zugriff hat und {{ appName }} ohne Passwort nutzen kann.
            Wie ein normaler Benutzer kann der Gastbenutzer nur auf seine Dokumente zugreifen und Berechtigungen zugreifen.
            ", "enable_guest_access": "Gastzugang aktivieren", "disable_guest_access": "Gastzugang deaktivieren", "title_theme": "Aussehen anpassen", @@ -358,36 +358,36 @@ "logo": "Logo (quadratische Größe) ", "background_image": "Hintergrundbild", "uploading_image": "Bild hochladen...", - "title_smtp": "Email configuration", + "title_smtp": "SMTP Email Einstellungen für Passwort wiederherstellungunread E-Mails und importieren sie automatisch.
            Nach dem Import einer E-Mail wird diese als gelesen markiert.
            Configuration settings for Gmail, Outlook.com, Yahoo.", + "message": "Wenn Sie diese Funktion aktivieren, durchsucht das System den angegebenen Posteingang jede Minute nach ungelesenen E-Mails und importieren diese automatisch.
            Nach dem Import einer E-Mail wird diese als gelesen markiert.
            Folgen Sie den Links zu Konfigurationseinstellungen für Gmail, Outlook.com, Yahoo.", "enabled": "Posteingang duchrsuchen aktivieren", "hostname": "IMAP Server", - "port": "IMAP Port (143 or 993)", + "port": "IMAP Port (143 oder 993)", "username": "IMAP Benutzername", "password": "IMAP Passwort", - "tag": "Tag zu importierten Dokumenten hinzugefügt", + "tag": "Folgenen Tag zu importierten Dokumenten hinzufügen", "test": "Konfiguration testen", - "last_sync": "Letzte Synchronisation: {{ data.date | date: 'medium' }}, {{ data.count }} E-Mail{{ data.count > 1 ? 's' : '' }} importiert", + "last_sync": "Letzte Synchronisation: {{ data.date | date: 'medium' }}, {{ data.count }} E-Mail(s){{ data.count > 1 ? 's' : '' }} importiert", "test_success": "Die Verbindung zum Posteingang war erfolgreich ({{ count }} unread message{{ count > 1 ? 's' : '' }})", "test_fail": "Beim Verbinden mit dem Posteingang ist ein Fehler aufgetreten, bitte überprüfen Sie die Einstellungen" }, "log": { - "title": "Server logs", + "title": "Server Logs", "date": "Datum", "tag": "Tag", "message": "Nachricht" }, "session": { - "title": "Geöffnet sessions", + "title": "Geöffnete Sitzungen", "created_date": "Erstellungsdatum", "last_connection_date": "Letztes Verbindungsdatum", "user_agent": "Von", @@ -397,10 +397,10 @@ "clear": "Alle anderen Sitzungen löschen" }, "vocabulary": { - "title": "Vokabular entries", + "title": "Vokabulareinträge", "choose_vocabulary": "Wählen Sie ein Vokabular aus, das Sie bearbeiten möchten.", "type": "Typ", - "coverage": "Geltungsbereich", + "Geltungsbereich": "Geltungsbereich", "rights": "Rechte", "value": "Wert", "order": "Reihenfolge", @@ -414,7 +414,7 @@ "need_2": "Ein Verzeichnis nach neuen Dateien durchsuchen lassen und gefunden Dateien importieren lassen möchten", "line_1": "Gehen Sie zu sismics/docs/releases und laden Sie das Datei-Importer-Tool für Ihr System herunter.", "line_2": "Folgen Sie dem Link instructions here um das Import-Toll zu nutzen.", - "line_3": "Ihre Dateien werden importiert in Quick upload, danach können Sie die Dateien weiterbearbeiten und Dokumenten zuordnen oder Dokumente erstellen.", + "line_3": "Ihre Dateien werden in importiert Quick upload, danach können Sie die Dateien weiterbearbeiten und Dokumenten zuordnen oder Dokumente erstellen.", "download": "Herunterladen", "instructions": "Anweisungen" } @@ -429,7 +429,7 @@ "title": "Wird importiert", "error_quota": "Speicher Limit erreicht, kontaktieren Sie Ihren Administrator, um den Ihnen zur verfügung gestellten Speicherplatz zu erhöhen. ", "error_general": "Beim Versuch, Ihre Datei zu importieren, ist ein Fehler aufgetreten. Bitte stellen Sie sicher, dass es sich um eine gültige EML-Datei handelt." -}, + }, "app_share": { "main": "Fragen Sie nach einen Link zu einem gemeinsam genutzten Dokument, um darauf zuzugreifen.", "403": { From 09eaf18632bd7bdee335987a175c13ddbe58a8af Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Fri, 9 Mar 2018 16:49:32 +0100 Subject: [PATCH 171/288] fix German translation (plural) --- docs-web/src/main/webapp/src/locale/de.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs-web/src/main/webapp/src/locale/de.json b/docs-web/src/main/webapp/src/locale/de.json index e0da9843..051b456a 100644 --- a/docs-web/src/main/webapp/src/locale/de.json +++ b/docs-web/src/main/webapp/src/locale/de.json @@ -79,7 +79,7 @@ "page_size_30": "30 pro Seite", "upgrade_quota": "Fragen Sie Ihren Administrator, um Ihr Speicherplatz zu erweitern.", "quota": "{{ current | number: 0 }}MB ({{ percent | number: 1 }}%) verwendet von {{ total | number: 0 }}MB", - "count": "{{ count }} Dokument{{ count > 1 ? 's' : '' }} gefunden", + "count": "{{ count }} Dokument{{ count > 1 ? 'e' : '' }} gefunden", "view": { "delete_comment_title": "Kommentar löschen", "delete_comment_message": "Möchten Sie diesen Kommentar wirklich löschen?", From c72f9fbdb1b55257e9fecd32bc4a8055bbdec447 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Fri, 9 Mar 2018 16:54:30 +0100 Subject: [PATCH 172/288] #176: search by tags and all associated children --- .../docs/core/dao/jpa/DocumentDao.java | 6 ++- .../com/sismics/docs/core/dao/jpa/TagDao.java | 4 -- .../core/dao/jpa/criteria/TagCriteria.java | 9 ---- .../com/sismics/docs/core/util/TagUtil.java | 54 +++++++++++++++++++ .../docs/rest/resource/DocumentResource.java | 12 +++-- .../sismics/docs/rest/TestTagResource.java | 35 +++++++++--- 6 files changed, 94 insertions(+), 26 deletions(-) create mode 100644 docs-core/src/main/java/com/sismics/docs/core/util/TagUtil.java diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/DocumentDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/DocumentDao.java index 25d6eb87..e88ea57a 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/DocumentDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/DocumentDao.java @@ -2,6 +2,7 @@ package com.sismics.docs.core.dao.jpa; import com.google.common.base.Joiner; import com.google.common.base.Strings; +import com.google.common.collect.Lists; import com.sismics.docs.core.constant.AuditLogType; import com.sismics.docs.core.constant.PermType; import com.sismics.docs.core.dao.jpa.criteria.DocumentCriteria; @@ -239,11 +240,14 @@ public class DocumentDao { } if (criteria.getTagIdList() != null && !criteria.getTagIdList().isEmpty()) { int index = 0; + List tagCriteriaList = Lists.newArrayList(); for (String tagId : criteria.getTagIdList()) { - sb.append(String.format(" join T_DOCUMENT_TAG dt%d on dt%d.DOT_IDDOCUMENT_C = d.DOC_ID_C and dt%d.DOT_IDTAG_C = :tagId%d and dt%d.DOT_DELETEDATE_D is null ", index, index, index, index, index)); + sb.append(String.format("left join T_DOCUMENT_TAG dt%d on dt%d.DOT_IDDOCUMENT_C = d.DOC_ID_C and dt%d.DOT_IDTAG_C = :tagId%d and dt%d.DOT_DELETEDATE_D is null ", index, index, index, index, index)); parameterMap.put("tagId" + index, tagId); + tagCriteriaList.add(String.format("dt%d.DOT_ID_C is not null", index)); index++; } + criteriaList.add(Joiner.on(" OR ").join(tagCriteriaList)); } if (criteria.getShared() != null && criteria.getShared()) { criteriaList.add("(select count(s.SHA_ID_C) from T_SHARE s, T_ACL ac where ac.ACL_SOURCEID_C = d.DOC_ID_C and ac.ACL_TARGETID_C = s.SHA_ID_C and ac.ACL_DELETEDATE_D is null and s.SHA_DELETEDATE_D is null) > 0"); diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/TagDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/TagDao.java index 6c276675..02b6d6f4 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/TagDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/TagDao.java @@ -195,10 +195,6 @@ public class TagDao { criteriaList.add("t.TAG_NAME_C = :name"); parameterMap.put("name", criteria.getName()); } - if (criteria.getNameLike() != null) { - criteriaList.add("t.TAG_NAME_C like :nameLike"); - parameterMap.put("nameLike", criteria.getNameLike() + "%"); - } criteriaList.add("t.TAG_DELETEDATE_D is null"); diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/criteria/TagCriteria.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/criteria/TagCriteria.java index 2665a205..6c52da84 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/criteria/TagCriteria.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/criteria/TagCriteria.java @@ -68,13 +68,4 @@ public class TagCriteria { this.name = name; return this; } - - public String getNameLike() { - return nameLike; - } - - public TagCriteria setNameLike(String nameLike) { - this.nameLike = nameLike; - return this; - } } diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/TagUtil.java b/docs-core/src/main/java/com/sismics/docs/core/util/TagUtil.java new file mode 100644 index 00000000..4933a618 --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/util/TagUtil.java @@ -0,0 +1,54 @@ +package com.sismics.docs.core.util; + +import com.google.common.collect.Lists; +import com.sismics.docs.core.dao.jpa.dto.TagDto; + +import java.util.List; + +/** + * Tag utilities. + * + * @author bgamard + */ +public class TagUtil { + /** + * Recursively find children of a tags. + * + * @param parentTagDto Parent tag + * @param allTagDtoList List of all tags + * @return Children tags + */ + public static List findChildren(TagDto parentTagDto, List allTagDtoList) { + List childrenTagDtoList = Lists.newArrayList(); + + for (TagDto tagDto : allTagDtoList) { + if (parentTagDto.getId().equals(tagDto.getParentId())) { + childrenTagDtoList.add(tagDto); + childrenTagDtoList.addAll(findChildren(tagDto, allTagDtoList)); + } + } + + return childrenTagDtoList; + } + + /** + * Find tags by name (start with). + * + * @param name Name + * @param allTagDtoList List of all tags + * @return List of filtered tags + */ + public static List findByName(String name, List allTagDtoList) { + List tagDtoList = Lists.newArrayList(); + if (name == null || name.isEmpty()) { + return tagDtoList; + } + name = name.toLowerCase(); + for (TagDto tagDto : allTagDtoList) { + if (tagDto.getName().toLowerCase().startsWith(name)) { + tagDtoList.add(tagDto); + } + } + return tagDtoList; + } +} diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/DocumentResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/DocumentResource.java index 0db1548b..cfcfcb71 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/DocumentResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/DocumentResource.java @@ -17,10 +17,7 @@ import com.sismics.docs.core.event.FileDeletedAsyncEvent; import com.sismics.docs.core.model.jpa.Document; import com.sismics.docs.core.model.jpa.File; import com.sismics.docs.core.model.jpa.User; -import com.sismics.docs.core.util.ConfigUtil; -import com.sismics.docs.core.util.DocumentUtil; -import com.sismics.docs.core.util.FileUtil; -import com.sismics.docs.core.util.PdfUtil; +import com.sismics.docs.core.util.*; import com.sismics.docs.core.util.jpa.PaginatedList; import com.sismics.docs.core.util.jpa.PaginatedLists; import com.sismics.docs.core.util.jpa.SortCriteria; @@ -424,6 +421,7 @@ public class DocumentResource extends BaseResource { } TagDao tagDao = new TagDao(); + List allTagDtoList = tagDao.findByCriteria(new TagCriteria().setTargetIdList(getTargetIdList(null)), null); UserDao userDao = new UserDao(); DateTimeParser[] parsers = { DateTimeFormat.forPattern("yyyy").getParser(), @@ -448,7 +446,7 @@ public class DocumentResource extends BaseResource { switch (params[0]) { case "tag": // New tag criteria - List tagDtoList = tagDao.findByCriteria(new TagCriteria().setTargetIdList(getTargetIdList(null)).setNameLike(params[1]), null); + List tagDtoList = TagUtil.findByName(params[1], allTagDtoList); if (documentCriteria.getTagIdList() == null) { documentCriteria.setTagIdList(new ArrayList()); } @@ -458,6 +456,10 @@ public class DocumentResource extends BaseResource { } for (TagDto tagDto : tagDtoList) { documentCriteria.getTagIdList().add(tagDto.getId()); + List childrenTagDtoList = TagUtil.findChildren(tagDto, allTagDtoList); + for (TagDto childrenTagDto : childrenTagDtoList) { + documentCriteria.getTagIdList().add(childrenTagDto.getId()); + } } break; case "after": diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestTagResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestTagResource.java index efd09d02..d84c11e1 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestTagResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestTagResource.java @@ -1,5 +1,9 @@ package com.sismics.docs.rest; +import com.sismics.util.filter.TokenBasedSecurityFilter; +import org.junit.Assert; +import org.junit.Test; + import javax.json.JsonArray; import javax.json.JsonObject; import javax.ws.rs.client.Entity; @@ -7,11 +11,6 @@ import javax.ws.rs.core.Form; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; -import org.junit.Assert; -import org.junit.Test; - -import com.sismics.util.filter.TokenBasedSecurityFilter; - /** * Test the tag resource. * @@ -66,12 +65,13 @@ public class TestTagResource extends BaseJerseyTest { Assert.assertEquals(Status.BAD_REQUEST, Status.fromStatusCode(response.getStatus())); // Create a document - target().path("/document").request() + json = target().path("/document").request() .cookie(TokenBasedSecurityFilter.COOKIE_NAME, tag1Token) .put(Entity.form(new Form() .param("title", "My super document 1") .param("tags", tag3Id) .param("language", "eng")), JsonObject.class); + String document1Id = json.getString("id"); // Create a document json = target().path("/document").request() @@ -81,7 +81,28 @@ public class TestTagResource extends BaseJerseyTest { .param("tags", tag4Id) .param("language", "eng")), JsonObject.class); String document2Id = json.getString("id"); - + + // Search document by parent tag + json = target().path("/document/list") + .queryParam("search", "tag:Tag3") + .queryParam("asc", "true") + .queryParam("sort_column", "1") + .request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, tag1Token) + .get(JsonObject.class); + Assert.assertEquals(2, json.getJsonArray("documents").size()); + Assert.assertEquals(document1Id, json.getJsonArray("documents").getJsonObject(0).getString("id")); + Assert.assertEquals(document2Id, json.getJsonArray("documents").getJsonObject(1).getString("id")); + + // Search document by children tag + json = target().path("/document/list") + .queryParam("search", "tag:Tag4") + .request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, tag1Token) + .get(JsonObject.class); + Assert.assertEquals(1, json.getJsonArray("documents").size()); + Assert.assertEquals(document2Id, json.getJsonArray("documents").getJsonObject(0).getString("id")); + // Check tags on a document json = target().path("/document/" + document2Id).request() .cookie(TokenBasedSecurityFilter.COOKIE_NAME, tag1Token) From de703531f6bca9ee5c170b7f92af90830d6a1c60 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Fri, 9 Mar 2018 17:11:50 +0100 Subject: [PATCH 173/288] fix for pdf generation and \r\n in description --- docs-core/src/main/java/com/sismics/docs/core/util/PdfUtil.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/PdfUtil.java b/docs-core/src/main/java/com/sismics/docs/core/util/PdfUtil.java index 1f3a79e6..302a68a8 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/util/PdfUtil.java +++ b/docs-core/src/main/java/com/sismics/docs/core/util/PdfUtil.java @@ -200,7 +200,7 @@ public class PdfUtil { .addText("Created by " + documentDto.getCreator() + " on " + dateFormat.format(new Date(documentDto.getCreateTimestamp())), true) .newLine() - .addText(documentDto.getDescription()) + .addText(Strings.nullToEmpty(documentDto.getDescription()).replaceAll("[\r\n]", "")) .newLine(); if (!Strings.isNullOrEmpty(documentDto.getSubject())) { pdfPage.addText("Subject: " + documentDto.getSubject()); From f167e8ea0a6b130e62558b26e0e86c63e6885a5d Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Fri, 9 Mar 2018 17:28:08 +0100 Subject: [PATCH 174/288] fix zip export --- .../main/java/com/sismics/docs/rest/resource/FileResource.java | 2 +- .../src/test/java/com/sismics/docs/rest/TestFileResource.java | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java index e283e584..84637018 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java @@ -584,7 +584,7 @@ public class FileResource extends BaseResource { // Files are encrypted by the creator of them User user = userDao.getById(file.getUserId()); try (InputStream decryptedStream = EncryptionUtil.decryptInputStream(fileInputStream, user.getPrivateKey())) { - ZipEntry zipEntry = new ZipEntry(file.getFullName(Integer.toString(index))); + ZipEntry zipEntry = new ZipEntry(index + "-" + file.getFullName(Integer.toString(index))); zipOutputStream.putNextEntry(zipEntry); ByteStreams.copy(decryptedStream, zipOutputStream); zipOutputStream.closeEntry(); diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestFileResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestFileResource.java index 0a5102fe..7939e044 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestFileResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestFileResource.java @@ -161,6 +161,7 @@ public class TestFileResource extends BaseJerseyTest { .request() .cookie(TokenBasedSecurityFilter.COOKIE_NAME, file1Token) .get(); + Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); is = (InputStream) response.getEntity(); fileBytes = ByteStreams.toByteArray(is); Assert.assertEquals(MimeType.APPLICATION_ZIP, MimeTypeUtil.guessMimeType(fileBytes, null)); From 6b940c43662897bbe3ad0e4bed9f8b830871f86c Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Fri, 9 Mar 2018 19:20:20 +0100 Subject: [PATCH 175/288] fix angular scope issue + disable LastPass in some forms --- docs-web/src/main/webapp/package-lock.json | 2467 +++++++++++++++++ .../controller/settings/SettingsUserEdit.js | 2 + .../main/webapp/src/partial/docs/login.html | 11 +- .../src/partial/docs/settings.account.html | 18 +- .../src/partial/docs/settings.inbox.html | 2 +- .../src/partial/docs/settings.user.edit.html | 40 +- 6 files changed, 2506 insertions(+), 34 deletions(-) create mode 100644 docs-web/src/main/webapp/package-lock.json diff --git a/docs-web/src/main/webapp/package-lock.json b/docs-web/src/main/webapp/package-lock.json new file mode 100644 index 00000000..60e38cab --- /dev/null +++ b/docs-web/src/main/webapp/package-lock.json @@ -0,0 +1,2467 @@ +{ + "name": "sismics-docs", + "version": "1.5.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "acorn": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-2.6.4.tgz", + "integrity": "sha1-6x9FtKQ/ox0DcBpexG87Umc+kO4=", + "dev": true + }, + "ajv": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", + "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", + "dev": true, + "optional": true, + "requires": { + "co": "4.6.0", + "json-stable-stringify": "1.0.1" + } + }, + "align-text": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", + "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", + "dev": true, + "requires": { + "kind-of": "3.2.2", + "longest": "1.0.1", + "repeat-string": "1.6.1" + } + }, + "alter": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/alter/-/alter-0.2.0.tgz", + "integrity": "sha1-x1iICGF1cgNKrmJICvJrHU0cs80=", + "dev": true, + "requires": { + "stable": "0.1.6" + } + }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", + "dev": true + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "apidoc": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/apidoc/-/apidoc-0.17.6.tgz", + "integrity": "sha1-TuisYQ3t3csQBsPij6fdY0tKXOY=", + "dev": true, + "requires": { + "apidoc-core": "0.8.3", + "fs-extra": "3.0.1", + "lodash": "4.17.5", + "markdown-it": "8.4.1", + "nomnom": "1.8.1", + "winston": "2.3.1" + } + }, + "apidoc-core": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/apidoc-core/-/apidoc-core-0.8.3.tgz", + "integrity": "sha1-2dY1RYKd8lDSzKBJaDqH53U2S5Y=", + "dev": true, + "requires": { + "fs-extra": "3.0.1", + "glob": "7.1.2", + "iconv-lite": "0.4.19", + "klaw-sync": "2.1.0", + "lodash": "4.17.5", + "semver": "5.3.0" + }, + "dependencies": { + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "semver": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", + "dev": true + } + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "1.0.3" + }, + "dependencies": { + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + } + } + }, + "array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", + "dev": true + }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=", + "dev": true, + "optional": true + }, + "asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=", + "dev": true, + "optional": true + }, + "assert-plus": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", + "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=", + "dev": true, + "optional": true + }, + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true, + "optional": true + }, + "aws-sign2": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", + "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=", + "dev": true, + "optional": true + }, + "aws4": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", + "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=", + "dev": true, + "optional": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", + "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "dev": true, + "optional": true, + "requires": { + "tweetnacl": "0.14.5" + } + }, + "boom": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", + "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", + "dev": true, + "requires": { + "hoek": "2.16.3" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "browserify-zlib": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.1.4.tgz", + "integrity": "sha1-uzX4pRn2AOD6a4SFJByXnQFB+y0=", + "dev": true, + "requires": { + "pako": "0.2.9" + } + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + }, + "camel-case": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz", + "integrity": "sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=", + "dev": true, + "requires": { + "no-case": "2.3.2", + "upper-case": "1.1.3" + } + }, + "camelcase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", + "dev": true + }, + "camelcase-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", + "dev": true, + "requires": { + "camelcase": "2.1.1", + "map-obj": "1.0.1" + } + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "dev": true, + "optional": true + }, + "center-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", + "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", + "dev": true, + "requires": { + "align-text": "0.1.4", + "lazy-cache": "1.0.4" + } + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + }, + "change-case": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-3.0.2.tgz", + "integrity": "sha512-Mww+SLF6MZ0U6kdg11algyKd5BARbyM4TbFBepwowYSR5ClfQGCGtxNXgykpN0uF/bstWeaGDT4JWaDh8zWAHA==", + "dev": true, + "requires": { + "camel-case": "3.0.0", + "constant-case": "2.0.0", + "dot-case": "2.1.1", + "header-case": "1.0.1", + "is-lower-case": "1.1.3", + "is-upper-case": "1.1.2", + "lower-case": "1.1.4", + "lower-case-first": "1.0.2", + "no-case": "2.3.2", + "param-case": "2.1.1", + "pascal-case": "2.0.1", + "path-case": "2.1.1", + "sentence-case": "2.1.1", + "snake-case": "2.1.0", + "swap-case": "1.1.2", + "title-case": "2.1.1", + "upper-case": "1.1.3", + "upper-case-first": "1.1.2" + } + }, + "clean-css": { + "version": "3.4.28", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-3.4.28.tgz", + "integrity": "sha1-vxlF6C/ICPVWlebd6uwBQA79A/8=", + "dev": true, + "requires": { + "commander": "2.8.1", + "source-map": "0.4.4" + }, + "dependencies": { + "commander": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz", + "integrity": "sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=", + "dev": true, + "requires": { + "graceful-readlink": "1.0.1" + } + } + } + }, + "cliui": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", + "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", + "dev": true, + "requires": { + "center-align": "0.1.3", + "right-align": "0.1.3", + "wordwrap": "0.0.2" + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true, + "optional": true + }, + "coffeescript": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/coffeescript/-/coffeescript-1.10.0.tgz", + "integrity": "sha1-56qDAZF+9iGzXYo580jc3R234z4=", + "dev": true + }, + "colors": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", + "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", + "dev": true + }, + "combined-stream": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", + "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", + "dev": true, + "requires": { + "delayed-stream": "1.0.0" + } + }, + "commander": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", + "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=", + "dev": true, + "requires": { + "graceful-readlink": "1.0.1" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "concat-stream": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.1.tgz", + "integrity": "sha512-gslSSJx03QKa59cIKqeJO9HQ/WZMotvYJCuaUULrLpjj8oG40kV2Z+gz82pVxlTkOADi4PJxQPPfhl1ELYrrXw==", + "dev": true, + "requires": { + "inherits": "2.0.3", + "readable-stream": "2.3.5", + "typedarray": "0.0.6" + } + }, + "constant-case": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/constant-case/-/constant-case-2.0.0.tgz", + "integrity": "sha1-QXV2TTidP6nI7NKRhu1gBSQ7akY=", + "dev": true, + "requires": { + "snake-case": "2.1.0", + "upper-case": "1.1.3" + } + }, + "convert-source-map": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.1.3.tgz", + "integrity": "sha1-SCnId+n+SbMWHzvzZziI4gRpmGA=", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "crc32": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/crc32/-/crc32-0.2.2.tgz", + "integrity": "sha1-etIg1v/c0Rn5/BJ6d3LKzqOQpLo=", + "dev": true + }, + "cryptiles": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", + "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", + "dev": true, + "optional": true, + "requires": { + "boom": "2.10.1" + } + }, + "csslint": { + "version": "0.9.10", + "resolved": "https://registry.npmjs.org/csslint/-/csslint-0.9.10.tgz", + "integrity": "sha1-xBuptrn+x3vKhxEuces6Ig71m8Q=", + "dev": true + }, + "currently-unhandled": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", + "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", + "dev": true, + "requires": { + "array-find-index": "1.0.2" + } + }, + "cycle": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", + "integrity": "sha1-IegLK+hYD5i0aPN5QwZisEbDStI=", + "dev": true + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, + "optional": true, + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true, + "optional": true + } + } + }, + "dateformat": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.12.tgz", + "integrity": "sha1-nxJLZ1lMk3/3BpMuSmQsyo27/uk=", + "dev": true, + "requires": { + "get-stdin": "4.0.1", + "meow": "3.7.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "deflate-js": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/deflate-js/-/deflate-js-0.2.3.tgz", + "integrity": "sha1-+Fq7WOvFFRowYUdHPVfD5PfkQms=", + "dev": true + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "dot-case": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-2.1.1.tgz", + "integrity": "sha1-NNzzf1Co6TwrO8qLt/uRVcfaO+4=", + "dev": true, + "requires": { + "no-case": "2.3.2" + } + }, + "ecc-jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "dev": true, + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, + "entities": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz", + "integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA=", + "dev": true + }, + "errno": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", + "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", + "dev": true, + "optional": true, + "requires": { + "prr": "1.0.1" + } + }, + "error-ex": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", + "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", + "dev": true, + "requires": { + "is-arrayish": "0.2.1" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "esprima": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", + "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", + "dev": true + }, + "eventemitter2": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", + "integrity": "sha1-j2G3XN4BKy6esoTUVFWDtWQ7Yas=", + "dev": true + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", + "dev": true + }, + "extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=", + "dev": true, + "optional": true + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "dev": true + }, + "eyes": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", + "integrity": "sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=", + "dev": true + }, + "figures": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", + "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", + "dev": true, + "requires": { + "escape-string-regexp": "1.0.5", + "object-assign": "4.1.1" + } + }, + "file-sync-cmp": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/file-sync-cmp/-/file-sync-cmp-0.1.1.tgz", + "integrity": "sha1-peeo/7+kk7Q7kju9TKiaU7Y7YSs=", + "dev": true + }, + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "dev": true, + "requires": { + "path-exists": "2.1.0", + "pinkie-promise": "2.0.1" + } + }, + "findup-sync": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.3.0.tgz", + "integrity": "sha1-N5MKpdgWt3fANEXhlmzGeQpMCxY=", + "dev": true, + "requires": { + "glob": "5.0.15" + }, + "dependencies": { + "glob": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", + "dev": true, + "requires": { + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + } + } + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true, + "optional": true + }, + "form-data": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", + "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", + "dev": true, + "optional": true, + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.6", + "mime-types": "2.1.18" + } + }, + "fs-extra": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-3.0.1.tgz", + "integrity": "sha1-N5TzeMWLNC6n27sjCVEJxLO2IpE=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "jsonfile": "3.0.1", + "universalify": "0.1.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "get-stdin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", + "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", + "dev": true + }, + "getobject": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/getobject/-/getobject-0.1.0.tgz", + "integrity": "sha1-BHpEl4n6Fg0Bj1SG7ZEyC27HiFw=", + "dev": true + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "optional": true, + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true, + "optional": true + } + } + }, + "glob": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.6.tgz", + "integrity": "sha1-IRuvr0nlJbjNkyYNFKsTYVKz9Xo=", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true + }, + "graceful-readlink": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", + "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", + "dev": true + }, + "grunt": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.0.2.tgz", + "integrity": "sha1-TmpeaVtwRy/VME9fqeNCNoNqc7w=", + "dev": true, + "requires": { + "coffeescript": "1.10.0", + "dateformat": "1.0.12", + "eventemitter2": "0.4.14", + "exit": "0.1.2", + "findup-sync": "0.3.0", + "glob": "7.0.6", + "grunt-cli": "1.2.0", + "grunt-known-options": "1.1.0", + "grunt-legacy-log": "1.0.1", + "grunt-legacy-util": "1.0.0", + "iconv-lite": "0.4.19", + "js-yaml": "3.5.5", + "minimatch": "3.0.4", + "nopt": "3.0.6", + "path-is-absolute": "1.0.1", + "rimraf": "2.2.8" + }, + "dependencies": { + "grunt-cli": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/grunt-cli/-/grunt-cli-1.2.0.tgz", + "integrity": "sha1-VisRnrsGndtGSs4oRVAb6Xs1tqg=", + "dev": true, + "requires": { + "findup-sync": "0.3.0", + "grunt-known-options": "1.1.0", + "nopt": "3.0.6", + "resolve": "1.1.7" + } + } + } + }, + "grunt-angular-templates": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/grunt-angular-templates/-/grunt-angular-templates-1.1.0.tgz", + "integrity": "sha1-EJYDorlf8BAZtxjHA0EmjwnYvhk=", + "dev": true, + "requires": { + "html-minifier": "2.1.7" + } + }, + "grunt-apidoc": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/grunt-apidoc/-/grunt-apidoc-0.11.0.tgz", + "integrity": "sha1-mMGUWtfoq6Hx1fFVHqs9QrAQ6s0=", + "dev": true, + "requires": { + "apidoc": "0.17.6" + } + }, + "grunt-cleanempty": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grunt-cleanempty/-/grunt-cleanempty-1.0.4.tgz", + "integrity": "sha1-V4OuhKAMeD4pDq3oQdK1biImIOo=", + "dev": true, + "requires": { + "junk": "1.0.3" + } + }, + "grunt-contrib-clean": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-clean/-/grunt-contrib-clean-1.1.0.tgz", + "integrity": "sha1-Vkq/LQN4qYOhW54/MO51tzjEBjg=", + "dev": true, + "requires": { + "async": "1.5.2", + "rimraf": "2.6.2" + }, + "dependencies": { + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "dev": true, + "requires": { + "glob": "7.0.6" + } + } + } + }, + "grunt-contrib-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/grunt-contrib-concat/-/grunt-contrib-concat-1.0.1.tgz", + "integrity": "sha1-YVCYYwhOhx1+ht5IwBUlntl3Rb0=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "source-map": "0.5.7" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "grunt-contrib-copy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-copy/-/grunt-contrib-copy-1.0.0.tgz", + "integrity": "sha1-cGDGWB6QS4qw0A8HbgqPbj58NXM=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "file-sync-cmp": "0.1.1" + } + }, + "grunt-contrib-less": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/grunt-contrib-less/-/grunt-contrib-less-1.4.1.tgz", + "integrity": "sha1-O73sC3XRLOqlXWKUNiXAsIYc328=", + "dev": true, + "requires": { + "async": "2.6.0", + "chalk": "1.1.3", + "less": "2.7.3", + "lodash": "4.17.5" + }, + "dependencies": { + "async": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.0.tgz", + "integrity": "sha512-xAfGg1/NTLBBKlHFmnd7PlmUW9KhVQIUuSrYem9xzFUZy13ScvtyGGejaae9iAVRiRq9+Cx7DPFaAAhCpyxyPw==", + "dev": true, + "requires": { + "lodash": "4.17.5" + } + } + } + }, + "grunt-contrib-uglify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/grunt-contrib-uglify/-/grunt-contrib-uglify-1.0.2.tgz", + "integrity": "sha1-rmekb5FT7dTLEYE6Vetpxw19svs=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "lodash": "4.17.5", + "maxmin": "1.1.0", + "uglify-js": "2.6.4", + "uri-path": "1.0.0" + } + }, + "grunt-css": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/grunt-css/-/grunt-css-0.5.4.tgz", + "integrity": "sha1-KW9rGXzZQSWcT79I6V6K+VsANU8=", + "dev": true, + "requires": { + "clean-css": "0.9.1", + "csslint": "0.9.10", + "gzip-js": "0.3.1" + }, + "dependencies": { + "clean-css": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-0.9.1.tgz", + "integrity": "sha1-SLIhUbkAVuE5qA1Mgk4PDzOsNgc=", + "dev": true, + "requires": { + "optimist": "0.3.7" + } + } + } + }, + "grunt-htmlrefs": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/grunt-htmlrefs/-/grunt-htmlrefs-0.5.0.tgz", + "integrity": "sha1-GkYOxsiQS4gr7EO+FCWj94xedTs=", + "dev": true + }, + "grunt-known-options": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/grunt-known-options/-/grunt-known-options-1.1.0.tgz", + "integrity": "sha1-pCdO6zL6dl2lp6OxcSYXzjsUQUk=", + "dev": true + }, + "grunt-legacy-log": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-1.0.1.tgz", + "integrity": "sha512-rwuyqNKlI0IPz0DvxzJjcEiQEBaBNVeb1LFoZKxSmHLETFUwhwUrqOsPIxURTKSwNZHZ4ht1YLBYmVU0YZAzHQ==", + "dev": true, + "requires": { + "colors": "1.1.2", + "grunt-legacy-log-utils": "1.0.0", + "hooker": "0.2.3", + "lodash": "4.17.5", + "underscore.string": "3.3.4" + } + }, + "grunt-legacy-log-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-1.0.0.tgz", + "integrity": "sha1-p7ji0Ps1taUPSvmG/BEnSevJbz0=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "lodash": "4.3.0" + }, + "dependencies": { + "lodash": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.3.0.tgz", + "integrity": "sha1-79nEpuxT87BUEkKZFcPkgk5NJaQ=", + "dev": true + } + } + }, + "grunt-legacy-util": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-1.0.0.tgz", + "integrity": "sha1-OGqnjcbtUJhsKxiVcmWxtIq7m4Y=", + "dev": true, + "requires": { + "async": "1.5.2", + "exit": "0.1.2", + "getobject": "0.1.0", + "hooker": "0.2.3", + "lodash": "4.3.0", + "underscore.string": "3.2.3", + "which": "1.2.14" + }, + "dependencies": { + "lodash": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.3.0.tgz", + "integrity": "sha1-79nEpuxT87BUEkKZFcPkgk5NJaQ=", + "dev": true + }, + "underscore.string": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.2.3.tgz", + "integrity": "sha1-gGmSYzZl1eX8tNsfs6hi62jp5to=", + "dev": true + } + } + }, + "grunt-ng-annotate": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/grunt-ng-annotate/-/grunt-ng-annotate-2.0.2.tgz", + "integrity": "sha1-SZPLr1aNUdHAw74K8EoIqCKZ0Uo=", + "dev": true, + "requires": { + "lodash.clonedeep": "4.5.0", + "ng-annotate": "1.2.2" + } + }, + "grunt-text-replace": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/grunt-text-replace/-/grunt-text-replace-0.4.0.tgz", + "integrity": "sha1-252c5Z4v5J2id+nbwZXD4Rz7FsI=", + "dev": true + }, + "gzip-js": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/gzip-js/-/gzip-js-0.3.1.tgz", + "integrity": "sha1-ejZ8TUCSEDMBAhiidAZZ24yvJ5I=", + "dev": true, + "requires": { + "crc32": "0.2.2", + "deflate-js": "0.2.3" + } + }, + "gzip-size": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-1.0.0.tgz", + "integrity": "sha1-Zs+LEBBHInuVus5uodoMF37Vwi8=", + "dev": true, + "requires": { + "browserify-zlib": "0.1.4", + "concat-stream": "1.6.1" + } + }, + "har-schema": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz", + "integrity": "sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4=", + "dev": true, + "optional": true + }, + "har-validator": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz", + "integrity": "sha1-M0gdDxu/9gDdID11gSpqX7oALio=", + "dev": true, + "optional": true, + "requires": { + "ajv": "4.11.8", + "har-schema": "1.0.5" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "has-color": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/has-color/-/has-color-0.1.7.tgz", + "integrity": "sha1-ZxRKUmDDT8PMpnfQQdr1L+e3iy8=", + "dev": true + }, + "hawk": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", + "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", + "dev": true, + "optional": true, + "requires": { + "boom": "2.10.1", + "cryptiles": "2.0.5", + "hoek": "2.16.3", + "sntp": "1.0.9" + } + }, + "he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", + "dev": true + }, + "header-case": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/header-case/-/header-case-1.0.1.tgz", + "integrity": "sha1-lTWXMZfBRLCWE81l0xfvGZY70C0=", + "dev": true, + "requires": { + "no-case": "2.3.2", + "upper-case": "1.1.3" + } + }, + "hoek": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", + "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=", + "dev": true + }, + "hooker": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz", + "integrity": "sha1-uDT3I8xKJCqmWWNFnfbZhMXT2Vk=", + "dev": true + }, + "hosted-git-info": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.6.0.tgz", + "integrity": "sha512-lIbgIIQA3lz5XaB6vxakj6sDHADJiZadYEJB+FgA+C4nubM1NwcuvUr9EJPmnH1skZqpqUzWborWo8EIUi0Sdw==", + "dev": true + }, + "html-minifier": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-2.1.7.tgz", + "integrity": "sha1-kFHW/LvPIU7TB+GtdPQyu5rWVcw=", + "dev": true, + "requires": { + "change-case": "3.0.2", + "clean-css": "3.4.28", + "commander": "2.9.0", + "he": "1.1.1", + "ncname": "1.0.0", + "relateurl": "0.2.7", + "uglify-js": "2.6.4" + } + }, + "http-signature": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", + "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", + "dev": true, + "optional": true, + "requires": { + "assert-plus": "0.2.0", + "jsprim": "1.4.1", + "sshpk": "1.13.1" + } + }, + "iconv-lite": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", + "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==", + "dev": true + }, + "image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=", + "dev": true, + "optional": true + }, + "indent-string": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", + "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", + "dev": true, + "requires": { + "repeating": "2.0.1" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-builtin-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", + "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", + "dev": true, + "requires": { + "builtin-modules": "1.1.1" + } + }, + "is-finite": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", + "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-lower-case": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/is-lower-case/-/is-lower-case-1.1.3.tgz", + "integrity": "sha1-fhR75HaNxGbbO/shzGCzHmrWk5M=", + "dev": true, + "requires": { + "lower-case": "1.1.4" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true, + "optional": true + }, + "is-upper-case": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-upper-case/-/is-upper-case-1.1.2.tgz", + "integrity": "sha1-jQsfp+eTOh5YSDYA7H2WYcuvdW8=", + "dev": true, + "requires": { + "upper-case": "1.1.3" + } + }, + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true + }, + "js-yaml": { + "version": "3.5.5", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.5.5.tgz", + "integrity": "sha1-A3fDgBfKvHMisNH7zSWkkWQfL74=", + "dev": true, + "requires": { + "argparse": "1.0.10", + "esprima": "2.7.3" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true, + "optional": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "dev": true, + "optional": true + }, + "json-stable-stringify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "dev": true, + "optional": true, + "requires": { + "jsonify": "0.0.0" + } + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true, + "optional": true + }, + "jsonfile": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz", + "integrity": "sha1-pezG9l9T9mLEQVx2daAzHQmS7GY=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11" + } + }, + "jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", + "dev": true, + "optional": true + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "dev": true, + "optional": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true, + "optional": true + } + } + }, + "junk": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/junk/-/junk-1.0.3.tgz", + "integrity": "sha1-h75jSIZJy9ym9Tqzm+yczSNH9ZI=", + "dev": true + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + }, + "klaw-sync": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-2.1.0.tgz", + "integrity": "sha1-PTvNhgDnv971MjHHOf8FOu1WDkQ=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11" + } + }, + "lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", + "dev": true + }, + "less": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/less/-/less-2.7.3.tgz", + "integrity": "sha512-KPdIJKWcEAb02TuJtaLrhue0krtRLoRoo7x6BNJIBelO00t/CCdJQUnHW5V34OnHMWzIktSalJxRO+FvytQlCQ==", + "dev": true, + "requires": { + "errno": "0.1.7", + "graceful-fs": "4.1.11", + "image-size": "0.5.5", + "mime": "1.6.0", + "mkdirp": "0.5.1", + "promise": "7.3.1", + "request": "2.81.0", + "source-map": "0.5.7" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true, + "optional": true + } + } + }, + "linkify-it": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.0.3.tgz", + "integrity": "sha1-2UpGSPmxwXnWT6lykSaL22zpQ08=", + "dev": true, + "requires": { + "uc.micro": "1.0.5" + } + }, + "load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "parse-json": "2.2.0", + "pify": "2.3.0", + "pinkie-promise": "2.0.1", + "strip-bom": "2.0.0" + } + }, + "lodash": { + "version": "4.17.5", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.5.tgz", + "integrity": "sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw==", + "dev": true + }, + "lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", + "dev": true + }, + "longest": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", + "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", + "dev": true + }, + "loud-rejection": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", + "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", + "dev": true, + "requires": { + "currently-unhandled": "0.4.1", + "signal-exit": "3.0.2" + } + }, + "lower-case": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", + "integrity": "sha1-miyr0bno4K6ZOkv31YdcOcQujqw=", + "dev": true + }, + "lower-case-first": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/lower-case-first/-/lower-case-first-1.0.2.tgz", + "integrity": "sha1-5dp8JvKacHO+AtUrrJmA5ZIq36E=", + "dev": true, + "requires": { + "lower-case": "1.1.4" + } + }, + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", + "dev": true + }, + "markdown-it": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-8.4.1.tgz", + "integrity": "sha512-CzzqSSNkFRUf9vlWvhK1awpJreMRqdCrBvZ8DIoDWTOkESMIF741UPAhuAmbyWmdiFPA6WARNhnu2M6Nrhwa+A==", + "dev": true, + "requires": { + "argparse": "1.0.10", + "entities": "1.1.1", + "linkify-it": "2.0.3", + "mdurl": "1.0.1", + "uc.micro": "1.0.5" + } + }, + "maxmin": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/maxmin/-/maxmin-1.1.0.tgz", + "integrity": "sha1-cTZehKmd2Piz99X94vANHn9zvmE=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "figures": "1.7.0", + "gzip-size": "1.0.0", + "pretty-bytes": "1.0.4" + } + }, + "mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=", + "dev": true + }, + "meow": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", + "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", + "dev": true, + "requires": { + "camelcase-keys": "2.1.0", + "decamelize": "1.2.0", + "loud-rejection": "1.6.0", + "map-obj": "1.0.1", + "minimist": "1.2.0", + "normalize-package-data": "2.4.0", + "object-assign": "4.1.1", + "read-pkg-up": "1.0.1", + "redent": "1.0.0", + "trim-newlines": "1.0.0" + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "optional": true + }, + "mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==", + "dev": true + }, + "mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "dev": true, + "requires": { + "mime-db": "1.33.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.11" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "optional": true, + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true, + "optional": true + } + } + }, + "ncname": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ncname/-/ncname-1.0.0.tgz", + "integrity": "sha1-W1etGLHKCShk72Kwse2BlPODtxw=", + "dev": true, + "requires": { + "xml-char-classes": "1.0.0" + } + }, + "ng-annotate": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/ng-annotate/-/ng-annotate-1.2.2.tgz", + "integrity": "sha1-3D/FG6Cy+LOF2+BH9NoG9YCh/WE=", + "dev": true, + "requires": { + "acorn": "2.6.4", + "alter": "0.2.0", + "convert-source-map": "1.1.3", + "optimist": "0.6.1", + "ordered-ast-traverse": "1.1.1", + "simple-fmt": "0.1.0", + "simple-is": "0.2.0", + "source-map": "0.5.7", + "stable": "0.1.6", + "stringmap": "0.2.2", + "stringset": "0.2.1", + "tryor": "0.1.2" + }, + "dependencies": { + "minimist": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", + "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", + "dev": true + }, + "optimist": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "dev": true, + "requires": { + "minimist": "0.0.10", + "wordwrap": "0.0.2" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "no-case": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz", + "integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==", + "dev": true, + "requires": { + "lower-case": "1.1.4" + } + }, + "nomnom": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/nomnom/-/nomnom-1.8.1.tgz", + "integrity": "sha1-IVH3Ikcrp55Qp2/BJbuMjy5Nwqc=", + "dev": true, + "requires": { + "chalk": "0.4.0", + "underscore": "1.6.0" + }, + "dependencies": { + "ansi-styles": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.0.0.tgz", + "integrity": "sha1-yxAt8cVvUSPquLZ817mAJ6AnkXg=", + "dev": true + }, + "chalk": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.4.0.tgz", + "integrity": "sha1-UZmj3c0MHv4jvAjBsCewYXbgxk8=", + "dev": true, + "requires": { + "ansi-styles": "1.0.0", + "has-color": "0.1.7", + "strip-ansi": "0.1.1" + } + }, + "strip-ansi": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.1.1.tgz", + "integrity": "sha1-OeipjQRNFQZgq+SmgIrPcLt7yZE=", + "dev": true + } + } + }, + "nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "dev": true, + "requires": { + "abbrev": "1.1.1" + } + }, + "normalize-package-data": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", + "dev": true, + "requires": { + "hosted-git-info": "2.6.0", + "is-builtin-module": "1.0.0", + "semver": "5.5.0", + "validate-npm-package-license": "3.0.3" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=", + "dev": true, + "optional": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "optimist": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.3.7.tgz", + "integrity": "sha1-yQlBrVnkJzMokjB00s8ufLxuwNk=", + "dev": true, + "requires": { + "wordwrap": "0.0.2" + } + }, + "ordered-ast-traverse": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ordered-ast-traverse/-/ordered-ast-traverse-1.1.1.tgz", + "integrity": "sha1-aEOhcLwO7otSDMjdwd3TqjD6BXw=", + "dev": true, + "requires": { + "ordered-esprima-props": "1.1.0" + } + }, + "ordered-esprima-props": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ordered-esprima-props/-/ordered-esprima-props-1.1.0.tgz", + "integrity": "sha1-qYJwht9fAQqmDpvQK24DNc6i/8s=", + "dev": true + }, + "pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU=", + "dev": true + }, + "param-case": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz", + "integrity": "sha1-35T9jPZTHs915r75oIWPvHK+Ikc=", + "dev": true, + "requires": { + "no-case": "2.3.2" + } + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "requires": { + "error-ex": "1.3.1" + } + }, + "pascal-case": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-2.0.1.tgz", + "integrity": "sha1-LVeNNFX2YNpl7KGO+VtODekSdh4=", + "dev": true, + "requires": { + "camel-case": "3.0.0", + "upper-case-first": "1.1.2" + } + }, + "path-case": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/path-case/-/path-case-2.1.1.tgz", + "integrity": "sha1-lLgDfDctP+KQbkZbtF4l0ibo7qU=", + "dev": true, + "requires": { + "no-case": "2.3.2" + } + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "dev": true, + "requires": { + "pinkie-promise": "2.0.1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" + } + }, + "performance-now": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", + "integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=", + "dev": true, + "optional": true + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "2.0.4" + } + }, + "pretty-bytes": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-1.0.4.tgz", + "integrity": "sha1-CiLoIQYJrTVUL4yNXSFZr/B1HIQ=", + "dev": true, + "requires": { + "get-stdin": "4.0.1", + "meow": "3.7.0" + } + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", + "dev": true + }, + "promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "dev": true, + "optional": true, + "requires": { + "asap": "2.0.6" + } + }, + "prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", + "dev": true, + "optional": true + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true, + "optional": true + }, + "qs": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", + "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=", + "dev": true, + "optional": true + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "dev": true, + "requires": { + "load-json-file": "1.1.0", + "normalize-package-data": "2.4.0", + "path-type": "1.1.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "dev": true, + "requires": { + "find-up": "1.1.2", + "read-pkg": "1.1.0" + } + }, + "readable-stream": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.5.tgz", + "integrity": "sha512-tK0yDhrkygt/knjowCUiWP9YdV7c5R+8cR0r/kt9ZhBU906Fs6RpQJCEilamRJj1Nx2rWI6LkW9gKqjTkshhEw==", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "2.0.0", + "safe-buffer": "5.1.1", + "string_decoder": "1.0.3", + "util-deprecate": "1.0.2" + } + }, + "redent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", + "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", + "dev": true, + "requires": { + "indent-string": "2.1.0", + "strip-indent": "1.0.1" + } + }, + "relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true + }, + "repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "dev": true, + "requires": { + "is-finite": "1.0.2" + } + }, + "request": { + "version": "2.81.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.81.0.tgz", + "integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=", + "dev": true, + "optional": true, + "requires": { + "aws-sign2": "0.6.0", + "aws4": "1.6.0", + "caseless": "0.12.0", + "combined-stream": "1.0.6", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.1.4", + "har-validator": "4.2.1", + "hawk": "3.1.3", + "http-signature": "1.1.1", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.18", + "oauth-sign": "0.8.2", + "performance-now": "0.2.0", + "qs": "6.4.0", + "safe-buffer": "5.1.1", + "stringstream": "0.0.5", + "tough-cookie": "2.3.4", + "tunnel-agent": "0.6.0", + "uuid": "3.2.1" + } + }, + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "dev": true + }, + "right-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", + "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", + "dev": true, + "requires": { + "align-text": "0.1.4" + } + }, + "rimraf": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", + "integrity": "sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI=", + "dev": true + }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", + "dev": true + }, + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", + "dev": true + }, + "sentence-case": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/sentence-case/-/sentence-case-2.1.1.tgz", + "integrity": "sha1-H24t2jnBaL+S0T+G1KkYkz9mftQ=", + "dev": true, + "requires": { + "no-case": "2.3.2", + "upper-case-first": "1.1.2" + } + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true + }, + "simple-fmt": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/simple-fmt/-/simple-fmt-0.1.0.tgz", + "integrity": "sha1-GRv1ZqWeZTBILLJatTtKjchcOms=", + "dev": true + }, + "simple-is": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/simple-is/-/simple-is-0.2.0.tgz", + "integrity": "sha1-Krt1qt453rXMgVzhDmGRFkhQuvA=", + "dev": true + }, + "snake-case": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-2.1.0.tgz", + "integrity": "sha1-Qb2xtz8w7GagTU4srRt2OH1NbZ8=", + "dev": true, + "requires": { + "no-case": "2.3.2" + } + }, + "sntp": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", + "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", + "dev": true, + "optional": true, + "requires": { + "hoek": "2.16.3" + } + }, + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "dev": true, + "requires": { + "amdefine": "1.0.1" + } + }, + "spdx-correct": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz", + "integrity": "sha512-N19o9z5cEyc8yQQPukRCZ9EUmb4HUpnrmaL/fxS2pBo2jbfcFRVuFZ/oFC+vZz0MNNk0h80iMn5/S6qGZOL5+g==", + "dev": true, + "requires": { + "spdx-expression-parse": "3.0.0", + "spdx-license-ids": "3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz", + "integrity": "sha512-4K1NsmrlCU1JJgUrtgEeTVyfx8VaYea9J9LvARxhbHtVtohPs/gFGG5yy49beySjlIMhhXZ4QqujIZEfS4l6Cg==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", + "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "dev": true, + "requires": { + "spdx-exceptions": "2.1.0", + "spdx-license-ids": "3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz", + "integrity": "sha512-2+EPwgbnmOIl8HjGBXXMd9NAu02vLjOO1nWw4kmeRDFyHn+M/ETfHxQUK0oXg8ctgVnl9t3rosNVsZ1jG61nDA==", + "dev": true + }, + "sprintf-js": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.1.tgz", + "integrity": "sha1-Nr54Mgr+WAH2zqPueLblqrlA6gw=", + "dev": true + }, + "sshpk": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", + "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", + "dev": true, + "optional": true, + "requires": { + "asn1": "0.2.3", + "assert-plus": "1.0.0", + "bcrypt-pbkdf": "1.0.1", + "dashdash": "1.14.1", + "ecc-jsbn": "0.1.1", + "getpass": "0.1.7", + "jsbn": "0.1.1", + "tweetnacl": "0.14.5" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true, + "optional": true + } + } + }, + "stable": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.6.tgz", + "integrity": "sha1-kQ9dKu17Ugxud3SZwfMuE5/eyxA=", + "dev": true + }, + "stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=", + "dev": true + }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "dev": true, + "requires": { + "safe-buffer": "5.1.1" + } + }, + "stringmap": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stringmap/-/stringmap-0.2.2.tgz", + "integrity": "sha1-VWwTeyWPlCuHdvWy71gqoGnX0bE=", + "dev": true + }, + "stringset": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/stringset/-/stringset-0.2.1.tgz", + "integrity": "sha1-7yWcTjSTRDd/zRyRPdLoSMnAQrU=", + "dev": true + }, + "stringstream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", + "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=", + "dev": true, + "optional": true + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "requires": { + "is-utf8": "0.2.1" + } + }, + "strip-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", + "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", + "dev": true, + "requires": { + "get-stdin": "4.0.1" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + }, + "swap-case": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/swap-case/-/swap-case-1.1.2.tgz", + "integrity": "sha1-w5IDpFhzhfrTyFCgvRvK+ggZdOM=", + "dev": true, + "requires": { + "lower-case": "1.1.4", + "upper-case": "1.1.3" + } + }, + "title-case": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/title-case/-/title-case-2.1.1.tgz", + "integrity": "sha1-PhJyFtpY0rxb7PE3q5Ha46fNj6o=", + "dev": true, + "requires": { + "no-case": "2.3.2", + "upper-case": "1.1.3" + } + }, + "tough-cookie": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", + "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", + "dev": true, + "optional": true, + "requires": { + "punycode": "1.4.1" + } + }, + "trim-newlines": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", + "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", + "dev": true + }, + "tryor": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/tryor/-/tryor-0.1.2.tgz", + "integrity": "sha1-gUXkynyv9ArN48z5Rui4u3W0Fys=", + "dev": true + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "5.1.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "dev": true, + "optional": true + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, + "uc.micro": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.5.tgz", + "integrity": "sha512-JoLI4g5zv5qNyT09f4YAvEZIIV1oOjqnewYg5D38dkQljIzpPT296dbIGvKro3digYI1bkb7W6EP1y4uDlmzLg==", + "dev": true + }, + "uglify-js": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.6.4.tgz", + "integrity": "sha1-ZeovswWck5RpLxX+2HwrNsFrmt8=", + "dev": true, + "requires": { + "async": "0.2.10", + "source-map": "0.5.7", + "uglify-to-browserify": "1.0.2", + "yargs": "3.10.0" + }, + "dependencies": { + "async": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", + "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "uglify-to-browserify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", + "dev": true + }, + "underscore": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz", + "integrity": "sha1-izixDKze9jM3uLJOT/htRa6lKag=", + "dev": true + }, + "underscore.string": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.3.4.tgz", + "integrity": "sha1-LCo/n4PmR2L9xF5s6sZRQoZCE9s=", + "dev": true, + "requires": { + "sprintf-js": "1.1.1", + "util-deprecate": "1.0.2" + } + }, + "universalify": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz", + "integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc=", + "dev": true + }, + "upper-case": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz", + "integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=", + "dev": true + }, + "upper-case-first": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/upper-case-first/-/upper-case-first-1.1.2.tgz", + "integrity": "sha1-XXm+3P8UQZUY/S7bCgUHybaFkRU=", + "dev": true, + "requires": { + "upper-case": "1.1.3" + } + }, + "uri-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/uri-path/-/uri-path-1.0.0.tgz", + "integrity": "sha1-l0fwGDWJM8Md4PzP2C0TjmcmLjI=", + "dev": true + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "uuid": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", + "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==", + "dev": true, + "optional": true + }, + "validate-npm-package-license": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz", + "integrity": "sha512-63ZOUnL4SIXj4L0NixR3L1lcjO38crAbgrTpl28t8jjrfuiOBL5Iygm+60qPs/KsZGzPNg6Smnc/oY16QTjF0g==", + "dev": true, + "requires": { + "spdx-correct": "3.0.0", + "spdx-expression-parse": "3.0.0" + } + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "dev": true, + "optional": true, + "requires": { + "assert-plus": "1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "1.3.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true, + "optional": true + } + } + }, + "which": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/which/-/which-1.2.14.tgz", + "integrity": "sha1-mofEN48D6CfOyvGs31bHNsAcFOU=", + "dev": true, + "requires": { + "isexe": "2.0.0" + } + }, + "window-size": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", + "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", + "dev": true + }, + "winston": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/winston/-/winston-2.3.1.tgz", + "integrity": "sha1-C0hCDZeMAYBM8CMLZIhhWYIloRk=", + "dev": true, + "requires": { + "async": "1.0.0", + "colors": "1.0.3", + "cycle": "1.0.3", + "eyes": "0.1.8", + "isstream": "0.1.2", + "stack-trace": "0.0.10" + }, + "dependencies": { + "async": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async/-/async-1.0.0.tgz", + "integrity": "sha1-+PwEyjoTeErenhZBr5hXjPvWR6k=", + "dev": true + }, + "colors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=", + "dev": true + } + } + }, + "wordwrap": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", + "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "xml-char-classes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/xml-char-classes/-/xml-char-classes-1.0.0.tgz", + "integrity": "sha1-ZGV4SKIP/F31g6Qq2KJ3tFErvE0=", + "dev": true + }, + "yargs": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", + "dev": true, + "requires": { + "camelcase": "1.2.1", + "cliui": "2.1.0", + "decamelize": "1.2.0", + "window-size": "0.1.0" + }, + "dependencies": { + "camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", + "dev": true + } + } + } + } +} diff --git a/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsUserEdit.js b/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsUserEdit.js index c7146b06..1b7b146b 100644 --- a/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsUserEdit.js +++ b/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsUserEdit.js @@ -19,6 +19,8 @@ angular.module('docs').controller('SettingsUserEdit', function($scope, $dialog, data.storage_quota /= 1000000; $scope.user = data; }); + } else { + $scope.user = {}; // Very important otherwise ng-if in template will make a new scope variable } /** 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 93154bf7..822ec9a1 100644 --- a/docs-web/src/main/webapp/src/partial/docs/login.html +++ b/docs-web/src/main/webapp/src/partial/docs/login.html @@ -34,12 +34,14 @@

            - +
            - +
            @@ -48,12 +50,13 @@
            - +
            diff --git a/docs-web/src/main/webapp/src/partial/docs/settings.account.html b/docs-web/src/main/webapp/src/partial/docs/settings.account.html index d59d679d..a1af2117 100644 --- a/docs-web/src/main/webapp/src/partial/docs/settings.account.html +++ b/docs-web/src/main/webapp/src/partial/docs/settings.account.html @@ -1,22 +1,22 @@

            - -
            - + +
            +
            -
            - {{ 'validation.required' | translate }} - {{ 'validation.too_short' | translate }} - {{ 'validation.too_long' | translate }} + {{ 'validation.required' | translate }} + {{ 'validation.too_short' | translate }} + {{ 'validation.too_long' | translate }}
            - +
            -
            diff --git a/docs-web/src/main/webapp/src/partial/docs/settings.inbox.html b/docs-web/src/main/webapp/src/partial/docs/settings.inbox.html index f200ebf6..90bb7cf6 100644 --- a/docs-web/src/main/webapp/src/partial/docs/settings.inbox.html +++ b/docs-web/src/main/webapp/src/partial/docs/settings.inbox.html @@ -65,7 +65,7 @@
            diff --git a/docs-web/src/main/webapp/src/partial/docs/settings.user.edit.html b/docs-web/src/main/webapp/src/partial/docs/settings.user.edit.html index 518e7446..9424207a 100644 --- a/docs-web/src/main/webapp/src/partial/docs/settings.user.edit.html +++ b/docs-web/src/main/webapp/src/partial/docs/settings.user.edit.html @@ -4,37 +4,37 @@

            - -
            - + +
            +
            -
            - {{ 'validation.required' | translate }} - {{ 'validation.too_short' | translate }} - {{ 'validation.too_long' | translate }} + {{ 'validation.required' | translate }} + {{ 'validation.too_short' | translate }} + {{ 'validation.too_long' | translate }}
            -
            +
            -
            - {{ 'validation.required' | translate }} - {{ 'validation.email' | translate }} - {{ 'validation.too_short' | translate }} - {{ 'validation.too_long' | translate }} + {{ 'validation.required' | translate }} + {{ 'validation.email' | translate }} + {{ 'validation.too_short' | translate }} + {{ 'validation.too_long' | translate }}
            -
            +
            @@ -60,17 +60,17 @@
            - + ng-class="{ 'has-error': !editUserForm.userPassword.$valid && editUserForm.$dirty, success: editUserForm.userPassword.$valid }"> +
            -
            - {{ 'validation.required' | translate }} - {{ 'validation.too_short' | translate }} - {{ 'validation.too_long' | translate }} + {{ 'validation.required' | translate }} + {{ 'validation.too_short' | translate }} + {{ 'validation.too_long' | translate }}
            From d497fa8ed740405eecba2a0b22ff8e728be2f279 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Fri, 9 Mar 2018 19:55:26 +0100 Subject: [PATCH 176/288] recreate a new imap session for each sync --- .../main/java/com/sismics/docs/core/service/InboxService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs-core/src/main/java/com/sismics/docs/core/service/InboxService.java b/docs-core/src/main/java/com/sismics/docs/core/service/InboxService.java index d3b2d263..fe576c54 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/service/InboxService.java +++ b/docs-core/src/main/java/com/sismics/docs/core/service/InboxService.java @@ -169,7 +169,7 @@ public class InboxService extends AbstractScheduledService { properties.put("mail.imap.writetimeout", 30000); } - Session session = Session.getDefaultInstance(properties); + Session session = Session.getInstance(properties); Store store = session.getStore("imap"); store.connect(ConfigUtil.getConfigStringValue(ConfigType.INBOX_USERNAME), From ce7a8590dba6e5bb51ea6fbd8d742a634a54e78b Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Fri, 9 Mar 2018 21:47:11 +0100 Subject: [PATCH 177/288] cache kill + hack to disable firefox autofill --- docs-web/src/main/webapp/Gruntfile.js | 3 +++ docs-web/src/main/webapp/src/index.html | 4 ++-- .../src/main/webapp/src/partial/docs/settings.user.edit.html | 1 + docs-web/src/main/webapp/src/share.html | 4 ++-- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/docs-web/src/main/webapp/Gruntfile.js b/docs-web/src/main/webapp/Gruntfile.js index 7c282d9c..6225d832 100644 --- a/docs-web/src/main/webapp/Gruntfile.js +++ b/docs-web/src/main/webapp/Gruntfile.js @@ -134,6 +134,9 @@ module.exports = function(grunt) { replacements: [{ from: '../api', to: grunt.option('apiurl') || '../api' + }, { + from: '@build.date@', + to: new Date().getTime() }] } }, diff --git a/docs-web/src/main/webapp/src/index.html b/docs-web/src/main/webapp/src/index.html index 65ec39a5..b1f8aa9e 100644 --- a/docs-web/src/main/webapp/src/index.html +++ b/docs-web/src/main/webapp/src/index.html @@ -7,7 +7,7 @@ - + @@ -25,7 +25,7 @@ }; - + diff --git a/docs-web/src/main/webapp/src/partial/docs/settings.user.edit.html b/docs-web/src/main/webapp/src/partial/docs/settings.user.edit.html index 9424207a..ab40ef13 100644 --- a/docs-web/src/main/webapp/src/partial/docs/settings.user.edit.html +++ b/docs-web/src/main/webapp/src/partial/docs/settings.user.edit.html @@ -63,6 +63,7 @@ ng-class="{ 'has-error': !editUserForm.userPassword.$valid && editUserForm.$dirty, success: editUserForm.userPassword.$valid }">
            +
            diff --git a/docs-web/src/main/webapp/src/share.html b/docs-web/src/main/webapp/src/share.html index 6ec3a6c6..5b60071c 100644 --- a/docs-web/src/main/webapp/src/share.html +++ b/docs-web/src/main/webapp/src/share.html @@ -7,7 +7,7 @@ - + @@ -24,7 +24,7 @@ }; - + From f227335e1455ea02bfd8a4d01174cbb376c147ff Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Fri, 9 Mar 2018 21:54:33 +0100 Subject: [PATCH 178/288] update German translation --- docs-web/src/main/webapp/src/locale/de.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs-web/src/main/webapp/src/locale/de.json b/docs-web/src/main/webapp/src/locale/de.json index 051b456a..e6e3cc49 100644 --- a/docs-web/src/main/webapp/src/locale/de.json +++ b/docs-web/src/main/webapp/src/locale/de.json @@ -263,7 +263,7 @@ "delete_user_message": "Möchten Sie diesen Benutzer wirklich löschen? Alle zugehörigen Dokumente, Dateien und Tags werden gelöscht", "edit_user_failed_title": "Dieser Benutzer existiert bereits", "edit_user_failed_message": "Dieser Benutzername wurde bereits von einem anderen Benutzer gewählt", - "edit_user_title": "Edit \"{{ username }}\"", + "edit_user_title": "Bearbeiten \"{{ username }}\"", "add_user_title": "Add a Benutzer", "username": "Benutzername", "email": "E-mail", @@ -286,7 +286,7 @@ "edit": { "delete_workflow_title": "Workflow löschen", "delete_workflow_message": "Möchten Sie diesen Workflow wirklich löschen? Derzeit ausgeführte Workflows werden nicht gelöscht", - "edit_workflow_title": "Edit \"{{ name }}\"", + "edit_workflow_title": "Bearbeiten \"{{ name }}\"", "add_workflow_title": "Add a Workflow", "name": "Name", "name_placeholder": "Name des Bearbeitungschritts oder der Beschreibung ", @@ -374,7 +374,7 @@ "port": "IMAP Port (143 oder 993)", "username": "IMAP Benutzername", "password": "IMAP Passwort", - "tag": "Folgenen Tag zu importierten Dokumenten hinzufügen", + "tag": "Folgenden Tag zu importierten Dokumenten hinzufügen", "test": "Konfiguration testen", "last_sync": "Letzte Synchronisation: {{ data.date | date: 'medium' }}, {{ data.count }} E-Mail(s){{ data.count > 1 ? 's' : '' }} importiert", "test_success": "Die Verbindung zum Posteingang war erfolgreich ({{ count }} unread message{{ count > 1 ? 's' : '' }})", From 8b039c61ed33a9b3f518c26b7aeb1f7c71b0c92e Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Fri, 9 Mar 2018 22:01:12 +0100 Subject: [PATCH 179/288] trim crappy characters --- .../com/sismics/docs/core/util/PdfUtil.java | 2 +- .../sismics/docs/core/util/pdf/PdfPage.java | 11 ++++++---- .../docs/rest/TestDocumentResource.java | 20 +++++++++---------- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/PdfUtil.java b/docs-core/src/main/java/com/sismics/docs/core/util/PdfUtil.java index 302a68a8..1f3a79e6 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/util/PdfUtil.java +++ b/docs-core/src/main/java/com/sismics/docs/core/util/PdfUtil.java @@ -200,7 +200,7 @@ public class PdfUtil { .addText("Created by " + documentDto.getCreator() + " on " + dateFormat.format(new Date(documentDto.getCreateTimestamp())), true) .newLine() - .addText(Strings.nullToEmpty(documentDto.getDescription()).replaceAll("[\r\n]", "")) + .addText(documentDto.getDescription()) .newLine(); if (!Strings.isNullOrEmpty(documentDto.getSubject())) { pdfPage.addText("Subject: " + documentDto.getSubject()); diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/pdf/PdfPage.java b/docs-core/src/main/java/com/sismics/docs/core/util/pdf/PdfPage.java index 0adae219..cb651cc3 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/util/pdf/PdfPage.java +++ b/docs-core/src/main/java/com/sismics/docs/core/util/pdf/PdfPage.java @@ -1,13 +1,13 @@ package com.sismics.docs.core.util.pdf; -import java.io.Closeable; -import java.io.IOException; - import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.font.PDFont; +import java.io.Closeable; +import java.io.IOException; + /** * Wrapper around PDFBox for high level abstraction of PDF writing. * @@ -102,7 +102,10 @@ public class PdfPage implements Closeable { if (text == null) { return; } - + + // Remove \r\n non breakable space + text = text.replaceAll("[\r\n]", "").replace("\u00A0", " "); + pdContent.setFont(font, fontSize); int start = 0; int end = 0; diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestDocumentResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestDocumentResource.java index 10589b5e..3ce2ceac 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestDocumentResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestDocumentResource.java @@ -251,14 +251,6 @@ public class TestDocumentResource extends BaseJerseyTest { Assert.assertTrue(relations.getJsonObject(0).getBoolean("source")); Assert.assertEquals("My super title document 1", relations.getJsonObject(0).getString("title")); - // Export a document in PDF format - Response response = target().path("/document/" + document1Id).request() - .cookie(TokenBasedSecurityFilter.COOKIE_NAME, document1Token) - .get(); - InputStream is = (InputStream) response.getEntity(); - byte[] pdfBytes = ByteStreams.toByteArray(is); - Assert.assertTrue(pdfBytes.length > 0); - // Create a tag json = target().path("/tag").request() .cookie(TokenBasedSecurityFilter.COOKIE_NAME, document1Token) @@ -271,7 +263,7 @@ public class TestDocumentResource extends BaseJerseyTest { .cookie(TokenBasedSecurityFilter.COOKIE_NAME, document1Token) .post(Entity.form(new Form() .param("title", "My new super document 1") - .param("description", "My new super description for document 1") + .param("description", "My new super description for document\r\n\u00A0 1") .param("subject", "My new subject for document 1") .param("identifier", "My new identifier for document 1") .param("publisher", "My new publisher for document 1") @@ -291,7 +283,15 @@ public class TestDocumentResource extends BaseJerseyTest { .param("title", "My super title document 2") .param("language", "eng")), JsonObject.class); Assert.assertEquals(document2Id, json.getString("id")); - + + // Export a document in PDF format + Response response = target().path("/document/" + document1Id).request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, document1Token) + .get(); + InputStream is = (InputStream) response.getEntity(); + byte[] pdfBytes = ByteStreams.toByteArray(is); + Assert.assertTrue(pdfBytes.length > 0); + // Search documents by query json = target().path("/document/list") .queryParam("search", "new") From f7b84238dfc01158ed249d2d3ee197b83031eddf Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Sat, 10 Mar 2018 10:30:15 +0100 Subject: [PATCH 180/288] update german translation --- docs-web/src/main/webapp/src/locale/de.json | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/docs-web/src/main/webapp/src/locale/de.json b/docs-web/src/main/webapp/src/locale/de.json index e6e3cc49..801c164e 100644 --- a/docs-web/src/main/webapp/src/locale/de.json +++ b/docs-web/src/main/webapp/src/locale/de.json @@ -232,7 +232,7 @@ "usergroup": { "search_groups": "In Gruppen suchen", "search_users": "In Benutzer suchen", - "you": "Du bist es!", + "you": "Eigenes Benutzerkonto!", "default": { "title": "Benutzer und Gruppen", "message": "Hier können Sie Informationen über Benutzer und Gruppen einsehen." @@ -263,8 +263,8 @@ "delete_user_message": "Möchten Sie diesen Benutzer wirklich löschen? Alle zugehörigen Dokumente, Dateien und Tags werden gelöscht", "edit_user_failed_title": "Dieser Benutzer existiert bereits", "edit_user_failed_message": "Dieser Benutzername wurde bereits von einem anderen Benutzer gewählt", - "edit_user_title": "Bearbeiten \"{{ username }}\"", - "add_user_title": "Add a Benutzer", + "edit_user_title": "Bearbeiten \"{{ username }}\"", + "add_user_title": "Neuen Benutzer hinzufügen", "username": "Benutzername", "email": "E-mail", "groups": "Gruppen", @@ -287,7 +287,7 @@ "delete_workflow_title": "Workflow löschen", "delete_workflow_message": "Möchten Sie diesen Workflow wirklich löschen? Derzeit ausgeführte Workflows werden nicht gelöscht", "edit_workflow_title": "Bearbeiten \"{{ name }}\"", - "add_workflow_title": "Add a Workflow", + "add_workflow_title": "Neuen Workflow hinzufügen", "name": "Name", "name_placeholder": "Name des Bearbeitungschritts oder der Beschreibung ", "drag_help": "Drag & drop, um die Schritte neu zu ordnen ", @@ -327,8 +327,8 @@ "delete_group_message": "Wollen Sie diese Gruppe wirklich löschen?", "edit_group_failed_title": "Gruppe existiert bereits", "edit_group_failed_message": "Dieser Gruppenname wird bereits von einer anderen Gruppe übernommen", - "edit_group_title": "Bearbeiten \"{{ name }}\"", - "add_group_title": "Add a Gruppe", + "edit_group_title": "Bearbeiten \"{{ name }}\"", + "add_group_title": "Neue Gruppe hinzufügen", "name": "Name", "parent_group": "Übergruppe", "search_group": "Gruppe suchen", @@ -414,7 +414,7 @@ "need_2": "Ein Verzeichnis nach neuen Dateien durchsuchen lassen und gefunden Dateien importieren lassen möchten", "line_1": "Gehen Sie zu sismics/docs/releases und laden Sie das Datei-Importer-Tool für Ihr System herunter.", "line_2": "Folgen Sie dem Link instructions here um das Import-Toll zu nutzen.", - "line_3": "Ihre Dateien werden in importiert Quick upload, danach können Sie die Dateien weiterbearbeiten und Dokumenten zuordnen oder Dokumente erstellen.", + "line_3": "Ihre Dateien werden in Quick upload importiert, danach können Sie die Dateien weiterbearbeiten und Dokumenten zuordnen oder Dokumente erstellen.", "download": "Herunterladen", "instructions": "Anweisungen" } @@ -497,8 +497,8 @@ "required": "Erfordert ", "too_short": "Zu kurz", "too_long": "Zu lang", - "email": "Muss eine gültige E-Mailadresse seinm", - "password_confirm": "Passwort und Passwort-Bestätigung müssen übereinstimmen", + "email": "Muss eine gültige E-Mailadresse sein", + "password_confirm": "Passwort und Passwortbestätigung müssen übereinstimmen", "number": "Nummer erfoderlich", "no_space": "Leerstellen sind nicht erlaubt" }, @@ -525,3 +525,4 @@ "enabled": "Eingeschaltet", "disabled": "ausgeschaltet" } + From 5cdbe9338b23dbdb3ad4b7a015c153f75dd43f82 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Sat, 10 Mar 2018 10:44:40 +0100 Subject: [PATCH 181/288] non crashing pdf font --- .../com/sismics/docs/core/util/PdfUtil.java | 8 +- .../sismics/docs/core/util/pdf/PdfPage.java | 15 +- .../pdfbox/pdmodel/font/DocsPDType1Font.java | 319 ++++++++++++++++++ .../docs/rest/TestDocumentResource.java | 2 +- 4 files changed, 330 insertions(+), 14 deletions(-) create mode 100644 docs-core/src/main/java/org/apache/pdfbox/pdmodel/font/DocsPDType1Font.java diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/PdfUtil.java b/docs-core/src/main/java/com/sismics/docs/core/util/PdfUtil.java index 1f3a79e6..445de127 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/util/PdfUtil.java +++ b/docs-core/src/main/java/com/sismics/docs/core/util/PdfUtil.java @@ -19,7 +19,7 @@ import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.common.PDRectangle; -import org.apache.pdfbox.pdmodel.font.PDType1Font; +import org.apache.pdfbox.pdmodel.font.DocsPDType1Font; import org.apache.pdfbox.pdmodel.graphics.image.JPEGFactory; import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory; import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; @@ -193,9 +193,9 @@ public class PdfUtil { if (metadata) { PDPage page = new PDPage(); doc.addPage(page); - try (PdfPage pdfPage = new PdfPage(doc, page, margin * mmPerInch, PDType1Font.HELVETICA, 12)) { + try (PdfPage pdfPage = new PdfPage(doc, page, margin * mmPerInch, DocsPDType1Font.HELVETICA, 12)) { SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); - pdfPage.addText(documentDto.getTitle(), true, PDType1Font.HELVETICA_BOLD, 16) + pdfPage.addText(documentDto.getTitle(), true, DocsPDType1Font.HELVETICA_BOLD, 16) .newLine() .addText("Created by " + documentDto.getCreator() + " on " + dateFormat.format(new Date(documentDto.getCreateTimestamp())), true) @@ -228,7 +228,7 @@ public class PdfUtil { } pdfPage.addText("Language: " + documentDto.getLanguage()) .newLine() - .addText("Files in this document : " + fileList.size(), false, PDType1Font.HELVETICA_BOLD, 12); + .addText("Files in this document : " + fileList.size(), false, DocsPDType1Font.HELVETICA_BOLD, 12); } } diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/pdf/PdfPage.java b/docs-core/src/main/java/com/sismics/docs/core/util/pdf/PdfPage.java index cb651cc3..b5098bc3 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/util/pdf/PdfPage.java +++ b/docs-core/src/main/java/com/sismics/docs/core/util/pdf/PdfPage.java @@ -28,7 +28,7 @@ public class PdfPage implements Closeable { * @param margin Margin * @param defaultFont Default font * @param defaultFontSize Default fond size - * @throws IOException + * @throws IOException e */ public PdfPage(PDDocument pdDoc, PDPage pdPage, float margin, PDFont defaultFont, int defaultFontSize) throws IOException { this.pdPage = pdPage; @@ -45,7 +45,7 @@ public class PdfPage implements Closeable { * Write a text with default font. * * @param text Text - * @throws IOException + * @throws IOException e */ public PdfPage addText(String text) throws IOException { drawText(pdPage.getMediaBox().getWidth() - 2 * margin, defaultFont, defaultFontSize, text, false); @@ -57,7 +57,7 @@ public class PdfPage implements Closeable { * * @param text Text * @param centered If true, the text will be centered in the page - * @throws IOException + * @throws IOException e */ public PdfPage addText(String text, boolean centered) throws IOException { drawText(pdPage.getMediaBox().getWidth() - 2 * margin, defaultFont, defaultFontSize, text, centered); @@ -71,7 +71,7 @@ public class PdfPage implements Closeable { * @param centered If true, the text will be centered in the page * @param font Font * @param fontSize Font size - * @throws IOException + * @throws IOException e */ public PdfPage addText(String text, boolean centered, PDFont font, int fontSize) throws IOException { drawText(pdPage.getMediaBox().getWidth() - 2 * margin, font, fontSize, text, centered); @@ -81,7 +81,7 @@ public class PdfPage implements Closeable { /** * Create a new line. * - * @throws IOException + * @throws IOException e */ public PdfPage newLine() throws IOException { pdContent.newLineAtOffset(0, - defaultFont.getFontDescriptor().getFontBoundingBox().getHeight() / 1000 * defaultFontSize); @@ -96,16 +96,13 @@ public class PdfPage implements Closeable { * @param fontSize Font size * @param text Text * @param centered If true, the text will be centered in the paragraph - * @throws IOException + * @throws IOException e */ private void drawText(float paragraphWidth, PDFont font, int fontSize, String text, boolean centered) throws IOException { if (text == null) { return; } - // Remove \r\n non breakable space - text = text.replaceAll("[\r\n]", "").replace("\u00A0", " "); - pdContent.setFont(font, fontSize); int start = 0; int end = 0; diff --git a/docs-core/src/main/java/org/apache/pdfbox/pdmodel/font/DocsPDType1Font.java b/docs-core/src/main/java/org/apache/pdfbox/pdmodel/font/DocsPDType1Font.java new file mode 100644 index 00000000..fa1c09c9 --- /dev/null +++ b/docs-core/src/main/java/org/apache/pdfbox/pdmodel/font/DocsPDType1Font.java @@ -0,0 +1,319 @@ +package org.apache.pdfbox.pdmodel.font; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.fontbox.EncodedFont; +import org.apache.fontbox.FontBoxFont; +import org.apache.fontbox.util.BoundingBox; +import org.apache.pdfbox.cos.COSName; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.font.encoding.*; +import org.apache.pdfbox.util.Matrix; + +import java.awt.geom.AffineTransform; +import java.awt.geom.GeneralPath; +import java.awt.geom.Point2D; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import static org.apache.pdfbox.pdmodel.font.UniUtil.getUniNameOfCodePoint; + +/** + * Safe non-crashing font even if no glyph are present. + * Will replace unknown glyphs by a space. + * + * @author bgamard + */ +public class DocsPDType1Font extends PDSimpleFont { + private static final Log LOG = LogFactory.getLog(DocsPDType1Font.class); + + // alternative names for glyphs which are commonly encountered + private static final Map ALT_NAMES = new HashMap<>(); + + static { + ALT_NAMES.put("ff", "f_f"); + ALT_NAMES.put("ffi", "f_f_i"); + ALT_NAMES.put("ffl", "f_f_l"); + ALT_NAMES.put("fi", "f_i"); + ALT_NAMES.put("fl", "f_l"); + ALT_NAMES.put("st", "s_t"); + ALT_NAMES.put("IJ", "I_J"); + ALT_NAMES.put("ij", "i_j"); + ALT_NAMES.put("ellipsis", "elipsis"); // misspelled in ArialMT + } + + public static final DocsPDType1Font HELVETICA = new DocsPDType1Font("Helvetica"); + public static final DocsPDType1Font HELVETICA_BOLD = new DocsPDType1Font("Helvetica-Bold"); + + /** + * embedded or system font for rendering. + */ + private final FontBoxFont genericFont; + + private final boolean isEmbedded; + private final boolean isDamaged; + private Matrix fontMatrix; + private final AffineTransform fontMatrixTransform; + private BoundingBox fontBBox; + + /** + * to improve encoding speed. + */ + private final Map codeToBytesMap; + + /** + * Creates a Type 1 standard 14 font for embedding. + * + * @param baseFont One of the standard 14 PostScript names + */ + private DocsPDType1Font(String baseFont) { + super(baseFont); + + dict.setItem(COSName.SUBTYPE, COSName.TYPE1); + dict.setName(COSName.BASE_FONT, baseFont); + if ("ZapfDingbats".equals(baseFont)) { + encoding = ZapfDingbatsEncoding.INSTANCE; + } else if ("Symbol".equals(baseFont)) { + encoding = SymbolEncoding.INSTANCE; + } else { + encoding = WinAnsiEncoding.INSTANCE; + dict.setItem(COSName.ENCODING, COSName.WIN_ANSI_ENCODING); + } + + // standard 14 fonts may be accessed concurrently, as they are singletons + codeToBytesMap = new ConcurrentHashMap<>(); + + FontMapping mapping = FontMappers.instance() + .getFontBoxFont(getBaseFont(), + getFontDescriptor()); + genericFont = mapping.getFont(); + + if (mapping.isFallback()) { + String fontName; + try { + fontName = genericFont.getName(); + } catch (IOException e) { + fontName = "?"; + } + LOG.warn("Using fallback font " + fontName + " for base font " + getBaseFont()); + } + isEmbedded = false; + isDamaged = false; + fontMatrixTransform = new AffineTransform(); + } + + /** + * Returns the PostScript name of the font. + */ + private String getBaseFont() { + return dict.getNameAsString(COSName.BASE_FONT); + } + + @Override + public float getHeight(int code) throws IOException { + String name = codeToName(code); + if (getStandard14AFM() != null) { + String afmName = getEncoding().getName(code); + return getStandard14AFM().getCharacterHeight(afmName); + } else { + return (float) genericFont.getPath(name).getBounds().getHeight(); + } + } + + @Override + protected byte[] encode(int unicode) throws IOException { + byte[] bytes = codeToBytesMap.get(unicode); + if (bytes != null) { + return bytes; + } + + String name = getGlyphList().codePointToName(unicode); + if (isStandard14()) { + // genericFont not needed, thus simplified code + // this is important on systems with no installed fonts + if (!encoding.contains(name)) { + return " ".getBytes(); + } + if (".notdef".equals(name)) { + return " ".getBytes(); + } + } else { + if (!encoding.contains(name)) { + return " ".getBytes(); + } + + String nameInFont = getNameInFont(name); + + if (nameInFont.equals(".notdef") || !genericFont.hasGlyph(nameInFont)) { + return " ".getBytes(); + } + } + + Map inverted = encoding.getNameToCodeMap(); + int code = inverted.get(name); + bytes = new byte[]{(byte) code}; + codeToBytesMap.put(code, bytes); + return bytes; + } + + @Override + public float getWidthFromFont(int code) throws IOException { + String name = codeToName(code); + + // width of .notdef is ignored for substitutes, see PDFBOX-1900 + if (!isEmbedded && ".notdef".equals(name)) { + return 250; + } + float width = genericFont.getWidth(name); + + Point2D p = new Point2D.Float(width, 0); + fontMatrixTransform.transform(p, p); + return (float) p.getX(); + } + + @Override + public boolean isEmbedded() { + return isEmbedded; + } + + @Override + public float getAverageFontWidth() { + if (getStandard14AFM() != null) { + return getStandard14AFM().getAverageCharacterWidth(); + } else { + return super.getAverageFontWidth(); + } + } + + @Override + public int readCode(InputStream in) throws IOException { + return in.read(); + } + + @Override + protected Encoding readEncodingFromFont() throws IOException { + if (!isEmbedded() && getStandard14AFM() != null) { + // read from AFM + return new Type1Encoding(getStandard14AFM()); + } else { + // extract from Type1 font/substitute + if (genericFont instanceof EncodedFont) { + return Type1Encoding.fromFontBox(((EncodedFont) genericFont).getEncoding()); + } else { + // default (only happens with TTFs) + return StandardEncoding.INSTANCE; + } + } + } + + @Override + public FontBoxFont getFontBoxFont() { + return genericFont; + } + + @Override + public String getName() { + return getBaseFont(); + } + + @Override + public BoundingBox getBoundingBox() throws IOException { + if (fontBBox == null) { + fontBBox = generateBoundingBox(); + } + return fontBBox; + } + + private BoundingBox generateBoundingBox() throws IOException { + if (getFontDescriptor() != null) { + PDRectangle bbox = getFontDescriptor().getFontBoundingBox(); + if (bbox != null && + (bbox.getLowerLeftX() != 0 || bbox.getLowerLeftY() != 0 || + bbox.getUpperRightX() != 0 || bbox.getUpperRightY() != 0)) { + return new BoundingBox(bbox.getLowerLeftX(), bbox.getLowerLeftY(), + bbox.getUpperRightX(), bbox.getUpperRightY()); + } + } + return genericFont.getFontBBox(); + } + + private String codeToName(int code) throws IOException { + String name = getEncoding().getName(code); + return getNameInFont(name); + } + + /** + * Maps a PostScript glyph name to the name in the underlying font, for example when + * using a TTF font we might map "W" to "uni0057". + */ + private String getNameInFont(String name) throws IOException { + if (isEmbedded() || genericFont.hasGlyph(name)) { + return name; + } else { + // try alternative name + String altName = ALT_NAMES.get(name); + if (altName != null && !name.equals(".notdef") && genericFont.hasGlyph(altName)) { + return altName; + } else { + // try unicode name + String unicodes = getGlyphList().toUnicode(name); + if (unicodes != null && unicodes.length() == 1) { + String uniName = getUniNameOfCodePoint(unicodes.codePointAt(0)); + if (genericFont.hasGlyph(uniName)) { + return uniName; + } + } + } + } + return ".notdef"; + } + + @Override + public GeneralPath getPath(String name) throws IOException { + // Acrobat does not draw .notdef for Type 1 fonts, see PDFBOX-2421 + // I suspect that it does do this for embedded fonts though, but this is untested + if (name.equals(".notdef") && !isEmbedded) { + return new GeneralPath(); + } else { + return genericFont.getPath(getNameInFont(name)); + } + } + + @Override + public boolean hasGlyph(String name) throws IOException { + return genericFont.hasGlyph(getNameInFont(name)); + } + + @Override + public final Matrix getFontMatrix() { + if (fontMatrix == null) { + // PDF specified that Type 1 fonts use a 1000upem matrix, but some fonts specify + // their own custom matrix anyway, for example PDFBOX-2298 + List numbers = null; + try { + numbers = genericFont.getFontMatrix(); + } catch (IOException e) { + fontMatrix = DEFAULT_FONT_MATRIX; + } + + if (numbers != null && numbers.size() == 6) { + fontMatrix = new Matrix( + numbers.get(0).floatValue(), numbers.get(1).floatValue(), + numbers.get(2).floatValue(), numbers.get(3).floatValue(), + numbers.get(4).floatValue(), numbers.get(5).floatValue()); + } else { + return super.getFontMatrix(); + } + } + return fontMatrix; + } + + @Override + public boolean isDamaged() { + return isDamaged; + } +} diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestDocumentResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestDocumentResource.java index 3ce2ceac..071051ad 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestDocumentResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestDocumentResource.java @@ -263,7 +263,7 @@ public class TestDocumentResource extends BaseJerseyTest { .cookie(TokenBasedSecurityFilter.COOKIE_NAME, document1Token) .post(Entity.form(new Form() .param("title", "My new super document 1") - .param("description", "My new super description for document\r\n\u00A0 1") + .param("description", "My new super description for document\r\n\u00A0\u0009 1") .param("subject", "My new subject for document 1") .param("identifier", "My new identifier for document 1") .param("publisher", "My new publisher for document 1") From a0e89103af0e4fb9756f4db2994996fb452e9f26 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Sat, 10 Mar 2018 11:24:06 +0100 Subject: [PATCH 182/288] cleanup duplicate code --- docs-web-common/pom.xml | 6 + .../sismics/docs/rest/util/ClientUtil.java | 22 ++++ .../docs/rest/TestDocumentResource.java | 117 +++--------------- 3 files changed, 42 insertions(+), 103 deletions(-) diff --git a/docs-web-common/pom.xml b/docs-web-common/pom.xml index 12b041f2..7d277514 100644 --- a/docs-web-common/pom.xml +++ b/docs-web-common/pom.xml @@ -100,6 +100,12 @@ test + + org.glassfish.jersey.media + jersey-media-multipart + test + + diff --git a/docs-web-common/src/test/java/com/sismics/docs/rest/util/ClientUtil.java b/docs-web-common/src/test/java/com/sismics/docs/rest/util/ClientUtil.java index ee6c733d..de999f55 100644 --- a/docs-web-common/src/test/java/com/sismics/docs/rest/util/ClientUtil.java +++ b/docs-web-common/src/test/java/com/sismics/docs/rest/util/ClientUtil.java @@ -1,13 +1,20 @@ package com.sismics.docs.rest.util; +import com.google.common.io.Resources; import com.sismics.util.filter.TokenBasedSecurityFilter; +import org.glassfish.jersey.media.multipart.FormDataMultiPart; +import org.glassfish.jersey.media.multipart.MultiPartFeature; +import org.glassfish.jersey.media.multipart.file.StreamDataBodyPart; import javax.json.JsonObject; import javax.ws.rs.client.Entity; import javax.ws.rs.client.WebTarget; import javax.ws.rs.core.Form; +import javax.ws.rs.core.MediaType; import javax.ws.rs.core.NewCookie; import javax.ws.rs.core.Response; +import java.io.IOException; +import java.io.InputStream; /** * REST client utilities. @@ -140,4 +147,19 @@ public class ClientUtil { } return authToken; } + + public String addFileToDocument(String file, String filename, String token, String documentId) throws IOException { + try (InputStream is = Resources.getResource(file).openStream()) { + StreamDataBodyPart streamDataBodyPart = new StreamDataBodyPart("file", is, filename); + try (FormDataMultiPart multiPart = new FormDataMultiPart()) { + JsonObject json = resource + .register(MultiPartFeature.class) + .path("/file").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, token) + .put(Entity.entity(multiPart.field("id", documentId).bodyPart(streamDataBodyPart), + MediaType.MULTIPART_FORM_DATA_TYPE), JsonObject.class); + return json.getString("id"); + } + } + } } diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestDocumentResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestDocumentResource.java index 071051ad..ff06e479 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestDocumentResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestDocumentResource.java @@ -85,21 +85,9 @@ public class TestDocumentResource extends BaseJerseyTest { Assert.assertNotNull(document2Id); // Add a file - String file1Id; - try (InputStream is = Resources.getResource("file/Einstein-Roosevelt-letter.png").openStream()) { - StreamDataBodyPart streamDataBodyPart = new StreamDataBodyPart("file", is, "Einstein-Roosevelt-letter.png"); - try (FormDataMultiPart multiPart = new FormDataMultiPart()) { - json = target() - .register(MultiPartFeature.class) - .path("/file").request() - .cookie(TokenBasedSecurityFilter.COOKIE_NAME, document1Token) - .put(Entity.entity(multiPart.field("id", document1Id).bodyPart(streamDataBodyPart), - MediaType.MULTIPART_FORM_DATA_TYPE), JsonObject.class); - file1Id = json.getString("id"); - Assert.assertNotNull(file1Id); - } - } - + String file1Id = clientUtil.addFileToDocument("file/Einstein-Roosevelt-letter.png", + "Einstein-Roosevelt-letter.png", document1Token, document1Id); + // Share this document target().path("/share").request() .cookie(TokenBasedSecurityFilter.COOKIE_NAME, document1Token) @@ -147,21 +135,9 @@ public class TestDocumentResource extends BaseJerseyTest { Assert.assertNotNull(document3Id); // Add a file - String file3Id; - try (InputStream is = Resources.getResource("file/Einstein-Roosevelt-letter.png").openStream()) { - StreamDataBodyPart streamDataBodyPart = new StreamDataBodyPart("file", is, "Einstein-Roosevelt-letter.png"); - try (FormDataMultiPart multiPart = new FormDataMultiPart()) { - json = target() - .register(MultiPartFeature.class) - .path("/file").request() - .cookie(TokenBasedSecurityFilter.COOKIE_NAME, document3Token) - .put(Entity.entity(multiPart.field("id", document3Id).bodyPart(streamDataBodyPart), - MediaType.MULTIPART_FORM_DATA_TYPE), JsonObject.class); - file3Id = json.getString("id"); - Assert.assertNotNull(file3Id); - } - } - + clientUtil.addFileToDocument("file/Einstein-Roosevelt-letter.png", + "Einstein-Roosevelt-letter.png", document3Token, document3Id); + // List all documents from document3 json = target().path("/document/list") .queryParam("sort_column", 3) @@ -392,21 +368,8 @@ public class TestDocumentResource extends BaseJerseyTest { Assert.assertNotNull(document1Id); // Add a PDF file - String file1Id; - try (InputStream is = Resources.getResource("file/document.odt").openStream()) { - StreamDataBodyPart streamDataBodyPart = new StreamDataBodyPart("file", is, "document.odt"); - try (FormDataMultiPart multiPart = new FormDataMultiPart()) { - json = target() - .register(MultiPartFeature.class) - .path("/file").request() - .cookie(TokenBasedSecurityFilter.COOKIE_NAME, documentOdtToken) - .put(Entity.entity(multiPart.field("id", document1Id).bodyPart(streamDataBodyPart), - MediaType.MULTIPART_FORM_DATA_TYPE), JsonObject.class); - file1Id = json.getString("id"); - Assert.assertNotNull(file1Id); - } - } - + String file1Id = clientUtil.addFileToDocument("file/document.odt", "document.odt", documentOdtToken, document1Id); + // Search documents by query in full content json = target().path("/document/list") .queryParam("search", "full:ipsum") @@ -451,21 +414,8 @@ public class TestDocumentResource extends BaseJerseyTest { Assert.assertNotNull(document1Id); // Add a PDF file - String file1Id; - try (InputStream is = Resources.getResource("file/document.docx").openStream()) { - StreamDataBodyPart streamDataBodyPart = new StreamDataBodyPart("file", is, "document.docx"); - try (FormDataMultiPart multiPart = new FormDataMultiPart()) { - json = target() - .register(MultiPartFeature.class) - .path("/file").request() - .cookie(TokenBasedSecurityFilter.COOKIE_NAME, documentDocxToken) - .put(Entity.entity(multiPart.field("id", document1Id).bodyPart(streamDataBodyPart), - MediaType.MULTIPART_FORM_DATA_TYPE), JsonObject.class); - file1Id = json.getString("id"); - Assert.assertNotNull(file1Id); - } - } - + String file1Id = clientUtil.addFileToDocument("file/document.docx", "document.docx", documentDocxToken, document1Id); + // Search documents by query in full content json = target().path("/document/list") .queryParam("search", "full:dolor") @@ -510,21 +460,8 @@ public class TestDocumentResource extends BaseJerseyTest { Assert.assertNotNull(document1Id); // Add a PDF file - String file1Id; - try (InputStream is = Resources.getResource("file/wikipedia.pdf").openStream()) { - StreamDataBodyPart streamDataBodyPart = new StreamDataBodyPart("file", is, "wikipedia.pdf"); - try (FormDataMultiPart multiPart = new FormDataMultiPart()) { - json = target() - .register(MultiPartFeature.class) - .path("/file").request() - .cookie(TokenBasedSecurityFilter.COOKIE_NAME, documentPdfToken) - .put(Entity.entity(multiPart.field("id", document1Id).bodyPart(streamDataBodyPart), - MediaType.MULTIPART_FORM_DATA_TYPE), JsonObject.class); - file1Id = json.getString("id"); - Assert.assertNotNull(file1Id); - } - } - + String file1Id = clientUtil.addFileToDocument("file/wikipedia.pdf", "wikipedia.pdf", documentPdfToken, document1Id); + // Search documents by query in full content json = target().path("/document/list") .queryParam("search", "full:vrandecic") @@ -569,20 +506,7 @@ public class TestDocumentResource extends BaseJerseyTest { Assert.assertNotNull(document1Id); // Add a plain text file - String file1Id; - try (InputStream is = Resources.getResource("file/document.txt").openStream()) { - StreamDataBodyPart streamDataBodyPart = new StreamDataBodyPart("file", is, "document.txt"); - try (FormDataMultiPart multiPart = new FormDataMultiPart()) { - json = target() - .register(MultiPartFeature.class) - .path("/file").request() - .cookie(TokenBasedSecurityFilter.COOKIE_NAME, documentPlainToken) - .put(Entity.entity(multiPart.field("id", document1Id).bodyPart(streamDataBodyPart), - MediaType.MULTIPART_FORM_DATA_TYPE), JsonObject.class); - file1Id = json.getString("id"); - Assert.assertNotNull(file1Id); - } - } + String file1Id = clientUtil.addFileToDocument("file/document.txt", "document.txt", documentPlainToken, document1Id); // Search documents by query in full content json = target().path("/document/list") @@ -628,20 +552,7 @@ public class TestDocumentResource extends BaseJerseyTest { Assert.assertNotNull(document1Id); // Add a video file - String file1Id; - try (InputStream is = Resources.getResource("file/video.webm").openStream()) { - StreamDataBodyPart streamDataBodyPart = new StreamDataBodyPart("file", is, "video.webm"); - try (FormDataMultiPart multiPart = new FormDataMultiPart()) { - json = target() - .register(MultiPartFeature.class) - .path("/file").request() - .cookie(TokenBasedSecurityFilter.COOKIE_NAME, documentPlainToken) - .put(Entity.entity(multiPart.field("id", document1Id).bodyPart(streamDataBodyPart), - MediaType.MULTIPART_FORM_DATA_TYPE), JsonObject.class); - file1Id = json.getString("id"); - Assert.assertNotNull(file1Id); - } - } + String file1Id = clientUtil.addFileToDocument("file/video.webm", "video.webm", documentPlainToken, document1Id); // Search documents by query in full content json = target().path("/document/list") From 77311f42cd9d2ab36896f70e8f48ab7e43b0c07b Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Sat, 10 Mar 2018 16:36:14 +0100 Subject: [PATCH 183/288] #176: navigation by tag --- .../docs/core/dao/jpa/DocumentDao.java | 2 +- .../docs/rest/resource/DocumentResource.java | 10 +- .../app/docs/controller/document/Document.js | 118 +++++++++++++++--- docs-web/src/main/webapp/src/locale/en.json | 2 + .../webapp/src/partial/docs/document.html | 57 +++++---- .../src/partial/docs/document.view.html | 54 ++++---- docs-web/src/main/webapp/src/style/main.less | 30 +++++ 7 files changed, 202 insertions(+), 71 deletions(-) diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/DocumentDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/DocumentDao.java index e88ea57a..c15c9624 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/DocumentDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/DocumentDao.java @@ -247,7 +247,7 @@ public class DocumentDao { tagCriteriaList.add(String.format("dt%d.DOT_ID_C is not null", index)); index++; } - criteriaList.add(Joiner.on(" OR ").join(tagCriteriaList)); + criteriaList.add("(" + Joiner.on(" OR ").join(tagCriteriaList) + ")"); } if (criteria.getShared() != null && criteria.getShared()) { criteriaList.add("(select count(s.SHA_ID_C) from T_SHARE s, T_ACL ac where ac.ACL_SOURCEID_C = d.DOC_ID_C and ac.ACL_TARGETID_C = s.SHA_ID_C and ac.ACL_DELETEDATE_D is null and s.SHA_DELETEDATE_D is null) > 0"); diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/DocumentResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/DocumentResource.java index cfcfcb71..74593c05 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/DocumentResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/DocumentResource.java @@ -499,14 +499,14 @@ public class DocumentResource extends BaseResource { break; case "shared": // New shared state criteria - if (params[1].equals("yes")) { - documentCriteria.setShared(true); - } + documentCriteria.setShared(params[1].equals("yes")); break; case "lang": // New language criteria if (Constants.SUPPORTED_LANGUAGES.contains(params[1])) { documentCriteria.setLanguage(params[1]); + } else { + documentCriteria.setLanguage(UUID.randomUUID().toString()); } break; case "by": @@ -522,9 +522,7 @@ public class DocumentResource extends BaseResource { break; case "workflow": // New shared state criteria - if (params[1].equals("me")) { - documentCriteria.setActiveRoute(true); - } + documentCriteria.setActiveRoute(params[1].equals("me")); break; case "full": // New full content search criteria diff --git a/docs-web/src/main/webapp/src/app/docs/controller/document/Document.js b/docs-web/src/main/webapp/src/app/docs/controller/document/Document.js index 4ec2df83..98d767a3 100644 --- a/docs-web/src/main/webapp/src/app/docs/controller/document/Document.js +++ b/docs-web/src/main/webapp/src/app/docs/controller/document/Document.js @@ -14,7 +14,6 @@ angular.module('docs').controller('Document', function ($scope, $rootScope, $tim $scope.limit = _.isUndefined(localStorage.documentsPageSize) ? '10' : localStorage.documentsPageSize; $scope.search = $state.params.search ? $state.params.search : ''; $scope.searchOpened = false; - $scope.setSearch = function (search) { $scope.search = search }; $scope.searchDropdownAnchor = angular.element(document.querySelector('.search-dropdown-anchor')); $scope.paginationShown = true; $scope.advsearch = {}; @@ -81,6 +80,8 @@ angular.module('docs').controller('Document', function ($scope, $rootScope, $tim }); } + $scope.extractNavigatedTag(); + // Call API later timeoutPromise = $timeout(function () { $scope.loadDocuments(); @@ -118,22 +119,6 @@ angular.module('docs').controller('Document', function ($scope, $rootScope, $tim $state.go('document.view', { id: id }); }; - // Load tags - $scope.tags = []; - Restangular.one('tag/list').get().then(function (data) { - $scope.tags = data.tags; - }); - - /** - * Find children tags. - * @param parent - */ - $scope.getChildrenTags = function(parent) { - return _.filter($scope.tags, function(tag) { - return tag.parent === parent; - }); - }; - /** * Returns a promise for typeahead user. */ @@ -210,12 +195,18 @@ angular.module('docs').controller('Document', function ($scope, $rootScope, $tim $scope.searchOpened = false; }; + /** + * Clear the search. + */ $scope.clearSearch = function () { $scope.advsearch = {}; $scope.search = ''; $scope.searchOpened = false; }; + /** + * Import an EML file. + */ $scope.importEml = function (file) { // Open the import modal $uibModal.open({ @@ -235,4 +226,97 @@ angular.module('docs').controller('Document', function ($scope, $rootScope, $tim $scope.loadDocuments(); }); }; + + // Tag navigation + $scope.tags = []; + $scope.navigatedFilter = { parent: '' }; + $scope.navigatedTag = undefined; + $scope.navigationEnabled = _.isUndefined(localStorage.navigationEnabled) ? + true : localStorage.navigationEnabled === 'true'; + + Restangular.one('tag/list').get().then(function (data) { + $scope.tags = data.tags; + $scope.extractNavigatedTag(); + }); + + /** + * Comparator for the navigation tag filter. + */ + $scope.navigatedComparator = function (actual, expected) { + if (expected === '') { + return _.isUndefined(actual); + } + return angular.equals(actual, expected); + }; + + /** + * Navigate to a specific tag. + */ + $scope.navigateToTag = function (tag) { + if (tag) { + $scope.search = 'tag:' + tag.name; + } else { + $scope.search = ''; + } + }; + + /** + * Navigate one tag up. + */ + $scope.navigateUp = function () { + if (!$scope.navigatedTag) { + return; + } + $scope.navigateToTag(_.findWhere($scope.tags, { id: $scope.navigatedTag.parent })); + }; + + /** + * Get the current navigation breadcrumb. + */ + $scope.getCurrentNavigation = function () { + if (!$scope.navigatedTag) { + return []; + } + + var nav = []; + nav.push($scope.navigatedTag); + var current = $scope.navigatedTag; + while (current.parent) { + current = _.findWhere($scope.tags, { id: current.parent }); + if (!current) { + break; + } + nav.push(current); + } + return nav.reverse(); + }; + + /** + * Extract the current navigated tag from the search query. + * Called each time the search query changes. + */ + $scope.extractNavigatedTag = function () { + // Find the current tag in the search query + var tagFound = /tag:([^ ]*)/.exec($scope.search); + if (tagFound) { + tagFound = tagFound[1]; + // We search only for exact match + $scope.navigatedTag = _.findWhere($scope.tags, { name: tagFound }); + } else { + $scope.navigatedTag = undefined; + } + if ($scope.navigatedTag) { + $scope.navigatedFilter = {parent: $scope.navigatedTag.id}; + } else { + $scope.navigatedFilter = {parent: ''}; + } + }; + + /** + * Toggle the navigation context. + */ + $scope.navigationToggle = function () { + $scope.navigationEnabled = !$scope.navigationEnabled; + localStorage.navigationEnabled = $scope.navigationEnabled; + }; }); \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/locale/en.json b/docs-web/src/main/webapp/src/locale/en.json index 35369d0d..e6ba5ae4 100644 --- a/docs-web/src/main/webapp/src/locale/en.json +++ b/docs-web/src/main/webapp/src/locale/en.json @@ -39,6 +39,8 @@ "global_quota_warning": "Warning! Global quota almost reached at {{ current | number: 0 }}MB ({{ percent | number: 1 }}%) used on {{ total | number: 0 }}MB" }, "document": { + "navigation_up": "Go up one level", + "toggle_navigation": "Toggle folder navigation", "search_simple": "Simple search", "search_fulltext": "Fulltext search", "search_creator": "Creator", diff --git a/docs-web/src/main/webapp/src/partial/docs/document.html b/docs-web/src/main/webapp/src/partial/docs/document.html index 205ade81..8c4f28f3 100644 --- a/docs-web/src/main/webapp/src/partial/docs/document.html +++ b/docs-web/src/main/webapp/src/partial/docs/document.html @@ -1,16 +1,7 @@ - -
            + +
            -
            - -
              -
            • {{ 'document.no_tags' | translate }}
            • -
            • -
            -
            - -
            +
            + +
            + +
            + + + +
            + + + + + + +
            {{ tag.name }}
            + + diff --git a/docs-web/src/main/webapp/src/partial/docs/document.view.html b/docs-web/src/main/webapp/src/partial/docs/document.view.html index 9f56996b..b309daf5 100644 --- a/docs-web/src/main/webapp/src/partial/docs/document.view.html +++ b/docs-web/src/main/webapp/src/partial/docs/document.view.html @@ -14,32 +14,34 @@
            -
            - -
            - +
            + + {{ 'edit' | translate }} +
            + + +
            - + +
            {{ tag.name }} +
            +
            + {{ tag.name }} + {{ getTagChildrenShort(tag) }} +
            diff --git a/docs-web/src/main/webapp/src/style/main.less b/docs-web/src/main/webapp/src/style/main.less index 1a35896b..b9ef2e75 100644 --- a/docs-web/src/main/webapp/src/style/main.less +++ b/docs-web/src/main/webapp/src/style/main.less @@ -36,12 +36,34 @@ // Navigation .table-navigation { + margin-left: 22px !important; + tr { cursor: pointer; td { border: none !important; } + + td.tree-structure { + padding: 0; + width: 1em; + border-left: 1px solid #aaa !important; + + .tree-line { + border-bottom: 1px solid #aaa !important; + border-left: 1px solid #aaa; + margin-left: -1px; + width: 100%; + height: 18px; + } + } + + &:last-child { + td.tree-structure { + border-left: 1px solid transparent !important; + } + } } } From ebfd860458e5d45ffe871a86cf11942aeb9e3555 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Sat, 10 Mar 2018 17:58:37 +0100 Subject: [PATCH 186/288] more tag tests --- .../sismics/docs/rest/TestTagResource.java | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestTagResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestTagResource.java index d84c11e1..a8f8fee1 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestTagResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestTagResource.java @@ -152,7 +152,7 @@ public class TestTagResource extends BaseJerseyTest { .cookie(TokenBasedSecurityFilter.COOKIE_NAME, tag1Token) .get(JsonObject.class); tags = json.getJsonArray("tags"); - Assert.assertTrue(tags.size() > 0); + Assert.assertTrue(tags.size() == 2); Assert.assertEquals("Tag4", tags.getJsonObject(1).getString("name")); Assert.assertEquals("#00ff00", tags.getJsonObject(1).getString("color")); Assert.assertEquals(tag3Id, tags.getJsonObject(1).getString("parent")); @@ -170,13 +170,30 @@ public class TestTagResource extends BaseJerseyTest { .cookie(TokenBasedSecurityFilter.COOKIE_NAME, tag1Token) .get(JsonObject.class); tags = json.getJsonArray("tags"); - Assert.assertTrue(tags.size() > 0); + Assert.assertTrue(tags.size() == 2); Assert.assertEquals("UpdatedName", tags.getJsonObject(1).getString("name")); Assert.assertEquals("#0000ff", tags.getJsonObject(1).getString("color")); Assert.assertNull(tags.getJsonObject(1).get("parent")); - + + // Update a tag + json = target().path("/tag/" + tag4Id).request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, tag1Token) + .post(Entity.form(new Form() + .param("name", "UpdatedName") + .param("color", "#0000ff") + .param("parent", tag3Id)), JsonObject.class); + Assert.assertEquals(tag4Id, json.getString("id")); + + // Get all tags + json = target().path("/tag/list").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, tag1Token) + .get(JsonObject.class); + tags = json.getJsonArray("tags"); + Assert.assertTrue(tags.size() == 2); + Assert.assertEquals(tag3Id, tags.getJsonObject(1).getString("parent")); + // Deletes a tag - target().path("/tag/" + tag4Id).request() + target().path("/tag/" + tag3Id).request() .cookie(TokenBasedSecurityFilter.COOKIE_NAME, tag1Token) .delete(); @@ -186,5 +203,7 @@ public class TestTagResource extends BaseJerseyTest { .get(JsonObject.class); tags = json.getJsonArray("tags"); Assert.assertTrue(tags.size() == 1); + Assert.assertEquals("UpdatedName", tags.getJsonObject(0).getString("name")); + Assert.assertNull(tags.getJsonObject(0).get("parent")); } } \ No newline at end of file From a66a1e6f8ed680b8049fdd256414c49b24d8f7e9 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Sun, 11 Mar 2018 00:02:00 +0100 Subject: [PATCH 187/288] i18n cache killer --- docs-web/src/main/webapp/src/app/docs/app.js | 2 +- docs-web/src/main/webapp/src/app/share/app.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs-web/src/main/webapp/src/app/docs/app.js b/docs-web/src/main/webapp/src/app/docs/app.js index 51bbb224..63638fbf 100644 --- a/docs-web/src/main/webapp/src/app/docs/app.js +++ b/docs-web/src/main/webapp/src/app/docs/app.js @@ -409,7 +409,7 @@ angular.module('docs', .useSanitizeValueStrategy(null) .useStaticFilesLoader({ prefix: 'locale/', - suffix: '.json' + suffix: '.json?@build.date@' }) .registerAvailableLanguageKeys(['en', 'fr', 'de', 'ru', 'zh_CN', 'zh_TW'], { 'ru_*': 'ru', diff --git a/docs-web/src/main/webapp/src/app/share/app.js b/docs-web/src/main/webapp/src/app/share/app.js index daf42538..63ba3d6d 100644 --- a/docs-web/src/main/webapp/src/app/share/app.js +++ b/docs-web/src/main/webapp/src/app/share/app.js @@ -59,7 +59,7 @@ angular.module('share', .useSanitizeValueStrategy(null) .useStaticFilesLoader({ prefix: 'locale/', - suffix: '.json' + suffix: '.json?@build.date@' }) .registerAvailableLanguageKeys(['en', 'fr', 'de', 'ru', 'zh_CN', 'zh_TW'], { 'ru_*': 'ru', From 647ad841dfd76c06d2f46bd26645737dd8a96129 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Mon, 12 Mar 2018 11:12:48 +0100 Subject: [PATCH 188/288] #186: ocr pdf if it contains no text --- .../async/FileCreatedAsyncListener.java | 1 - .../com/sismics/docs/core/util/FileUtil.java | 45 +++++++++++------- .../com/sismics/docs/core/util/PdfUtil.java | 31 +++++++----- .../sismics/docs/core/util/TestFileUtil.java | 25 ++++++++-- docs-core/src/test/resources/file/scanned.pdf | Bin 0 -> 772000 bytes 5 files changed, 69 insertions(+), 33 deletions(-) create mode 100644 docs-core/src/test/resources/file/scanned.pdf diff --git a/docs-core/src/main/java/com/sismics/docs/core/listener/async/FileCreatedAsyncListener.java b/docs-core/src/main/java/com/sismics/docs/core/listener/async/FileCreatedAsyncListener.java index b476d1c1..eb5bf27e 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/listener/async/FileCreatedAsyncListener.java +++ b/docs-core/src/main/java/com/sismics/docs/core/listener/async/FileCreatedAsyncListener.java @@ -27,7 +27,6 @@ public class FileCreatedAsyncListener { * File created. * * @param fileCreatedAsyncEvent File created event - * @throws Exception e */ @Subscribe public void on(final FileCreatedAsyncEvent fileCreatedAsyncEvent) { diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/FileUtil.java b/docs-core/src/main/java/com/sismics/docs/core/util/FileUtil.java index 3da17017..d821f3c5 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/util/FileUtil.java +++ b/docs-core/src/main/java/com/sismics/docs/core/util/FileUtil.java @@ -58,31 +58,22 @@ public class FileUtil { } else if (VideoUtil.isVideo(file.getMimeType())) { content = VideoUtil.getMetadata(unencryptedFile); } else if (unencryptedPdfFile != null) { - content = PdfUtil.extractPdf(unencryptedPdfFile); + content = PdfUtil.extractPdf(unencryptedPdfFile, language); } return content; } - + /** - * Optical character recognition on a file. - * - * @param unecryptedFile Unencrypted file + * Optical character recognition on an image. + * + * @param image Buffered image * @param language Language to OCR * @return Content extracted */ - private static String ocrFile(Path unecryptedFile, String language) { - Tesseract instance = Tesseract.getInstance(); - String content = null; - BufferedImage image; - try (InputStream inputStream = Files.newInputStream(unecryptedFile)) { - image = ImageIO.read(inputStream); - } catch (IOException e) { - log.error("Error reading the image", e); - return null; - } - + public static String ocrFile(BufferedImage image, String language) { // Upscale, grayscale and deskew the image + String content = null; BufferedImage resizedImage = Scalr.resize(image, Scalr.Method.AUTOMATIC, Scalr.Mode.AUTOMATIC, 3500, Scalr.OP_ANTIALIAS, Scalr.OP_GRAYSCALE); image.flush(); ImageDeskew imageDeskew = new ImageDeskew(resizedImage); @@ -92,15 +83,35 @@ public class FileUtil { // OCR the file try { + Tesseract instance = Tesseract.getInstance(); log.info("Starting OCR with TESSDATA_PREFIX=" + System.getenv("TESSDATA_PREFIX") + ";LC_NUMERIC=" + System.getenv("LC_NUMERIC")); instance.setLanguage(language); content = instance.doOCR(image); } catch (Throwable e) { log.error("Error while OCR-izing the image", e); } - + return content; } + + /** + * Optical character recognition on a file. + * + * @param unecryptedFile Unencrypted file + * @param language Language to OCR + * @return Content extracted + */ + private static String ocrFile(Path unecryptedFile, String language) { + BufferedImage image; + try (InputStream inputStream = Files.newInputStream(unecryptedFile)) { + image = ImageIO.read(inputStream); + } catch (IOException e) { + log.error("Error reading the image", e); + return null; + } + + return ocrFile(image, language); + } /** * Save a file on the storage filesystem. diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/PdfUtil.java b/docs-core/src/main/java/com/sismics/docs/core/util/PdfUtil.java index 445de127..decc88b7 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/util/PdfUtil.java +++ b/docs-core/src/main/java/com/sismics/docs/core/util/PdfUtil.java @@ -59,24 +59,31 @@ public class PdfUtil { * Extract text from a PDF. * * @param unencryptedPdfFile Unencrypted PDF file + * @param language Language * @return Content extracted */ - public static String extractPdf(Path unencryptedPdfFile) { + public static String extractPdf(Path unencryptedPdfFile, String language) { String content = null; - PDDocument pdfDocument = null; - try (InputStream inputStream = Files.newInputStream(unencryptedPdfFile)) { - PDFTextStripper stripper = new PDFTextStripper(); - pdfDocument = PDDocument.load(inputStream); - content = stripper.getText(pdfDocument); + try (InputStream inputStream = Files.newInputStream(unencryptedPdfFile); + PDDocument pdfDocument = PDDocument.load(inputStream)) { + content = new PDFTextStripper().getText(pdfDocument); } catch (Exception e) { log.error("Error while extracting text from the PDF", e); - } finally { - if (pdfDocument != null) { - try { - pdfDocument.close(); - } catch (IOException e) { - // NOP + } + + // No text content, try to OCR it + if (language != null && content != null && content.trim().isEmpty()) { + StringBuilder sb = new StringBuilder(); + try (InputStream inputStream = Files.newInputStream(unencryptedPdfFile); + PDDocument pdfDocument = PDDocument.load(inputStream)) { + PDFRenderer renderer = new PDFRenderer(pdfDocument); + for (int pageIndex = 0; pageIndex < pdfDocument.getNumberOfPages(); pageIndex++) { + sb.append(" "); + sb.append(FileUtil.ocrFile(renderer.renderImage(pageIndex), language)); } + return sb.toString(); + } catch (Exception e) { + log.error("Error while OCR-izing the PDF", e); } } diff --git a/docs-core/src/test/java/com/sismics/docs/core/util/TestFileUtil.java b/docs-core/src/test/java/com/sismics/docs/core/util/TestFileUtil.java index 0417f691..9d3ca268 100644 --- a/docs-core/src/test/java/com/sismics/docs/core/util/TestFileUtil.java +++ b/docs-core/src/test/java/com/sismics/docs/core/util/TestFileUtil.java @@ -17,7 +17,7 @@ import java.nio.file.StandardCopyOption; import java.util.Date; /** - * Test of the file entity utilities. + * Test of the file utilities. * * @author bgamard */ @@ -41,7 +41,26 @@ public class TestFileUtil { String content = FileUtil.extractContent(null, file, path, pdfPath); Assert.assertTrue(content.contains("Lorem ipsum dolor sit amen.")); } - + + @Test + public void extractContentPdf() throws Exception { + Path path = Paths.get(ClassLoader.getSystemResource("file/udhr.pdf").toURI()); + File file = new File(); + file.setMimeType(MimeType.APPLICATION_PDF); + String content = FileUtil.extractContent(null, file, path, path); + Assert.assertTrue(content.contains("All human beings are born free and equal in dignity and rights.")); + } + + @Test + public void extractContentScannedPdf() throws Exception { + Path path = Paths.get(ClassLoader.getSystemResource("file/scanned.pdf").toURI()); + File file = new File(); + file.setMimeType(MimeType.APPLICATION_PDF); + String content = FileUtil.extractContent("eng", file, path, path); + System.out.println(content); + Assert.assertTrue(content.contains("All human beings are born free and equal in dignity and rights.")); + } + @Test public void convertToPdfTest() throws Exception { try (InputStream inputStream0 = Resources.getResource("file/apollo_landscape.jpg").openStream(); @@ -52,7 +71,7 @@ public class TestFileUtil { // Document DocumentDto documentDto = new DocumentDto(); documentDto.setTitle("My super document 1"); - documentDto.setDescription("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis id turpis iaculis, commodo est ac, efficitur quam. Nam accumsan magna in orci vulputate ultricies. Sed vulputate neque magna, at laoreet leo ultricies vel. Proin eu hendrerit felis. Quisque sit amet arcu efficitur, pulvinar orci sed, imperdiet elit. Nunc posuere ex sed fermentum congue. Aliquam ultrices convallis finibus. Praesent iaculis justo vitae dictum auctor. Praesent suscipit imperdiet erat ac maximus. Aenean pharetra quam sed fermentum commodo. Donec sagittis ipsum nibh, id congue dolor venenatis quis. In tincidunt nisl non ex sollicitudin, a imperdiet neque scelerisque. Nullam lacinia ac orci sed faucibus. Donec tincidunt venenatis justo, nec fermentum justo rutrum a."); + documentDto.setDescription("Lorem ipsum dolor sit amet, consectetur adipiscing elit.\r\n Duis id turpis iaculis, commodo est ac, efficitur quam.\t Nam accumsan magna in orci vulputate ultricies. Sed vulputate neque magna, at laoreet leo ultricies vel. Proin eu hendrerit felis. Quisque sit amet arcu efficitur, pulvinar orci sed, imperdiet elit. Nunc posuere ex sed fermentum congue. Aliquam ultrices convallis finibus. Praesent iaculis justo vitae dictum auctor. Praesent suscipit imperdiet erat ac maximus. Aenean pharetra quam sed fermentum commodo. Donec sagittis ipsum nibh, id congue dolor venenatis quis. In tincidunt nisl non ex sollicitudin, a imperdiet neque scelerisque. Nullam lacinia ac orci sed faucibus. Donec tincidunt venenatis justo, nec fermentum justo rutrum a."); documentDto.setSubject("A set of random picture"); documentDto.setIdentifier("ID-2016-08-00001"); documentDto.setPublisher("My Publisher, Inc."); diff --git a/docs-core/src/test/resources/file/scanned.pdf b/docs-core/src/test/resources/file/scanned.pdf new file mode 100644 index 0000000000000000000000000000000000000000..b8f9e1f3067dd7c1236a0f614ff8172e5a80201c GIT binary patch literal 772000 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
            Ke@l6XiI*oo!ZlrW`t(dm&_8tgx2<|b;o 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 z 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 Z6&&Ib6!Hh0U`zVC2Exk9<|qr{{{k?$q5%K^ literal 0 HcmV?d00001 From 9d8034e0102194a9d5c472bff5af4acb4a533dd3 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Mon, 12 Mar 2018 11:43:11 +0100 Subject: [PATCH 189/288] clickable labels --- .../async/DocumentUpdatedAsyncListener.java | 1 - .../async/FileDeletedAsyncListener.java | 7 ++-- .../async/RouteStepValidateAsyncListener.java | 2 +- .../com/sismics/util/HtmlToPlainText.java | 40 +------------------ .../app/docs/controller/document/Document.js | 1 + .../src/partial/docs/document.view.html | 2 +- 6 files changed, 7 insertions(+), 46 deletions(-) diff --git a/docs-core/src/main/java/com/sismics/docs/core/listener/async/DocumentUpdatedAsyncListener.java b/docs-core/src/main/java/com/sismics/docs/core/listener/async/DocumentUpdatedAsyncListener.java index 2d208970..a3cdfcf5 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/listener/async/DocumentUpdatedAsyncListener.java +++ b/docs-core/src/main/java/com/sismics/docs/core/listener/async/DocumentUpdatedAsyncListener.java @@ -27,7 +27,6 @@ public class DocumentUpdatedAsyncListener { * Document updated. * * @param event Document updated event - * @throws Exception */ @Subscribe public void on(final DocumentUpdatedAsyncEvent event) { diff --git a/docs-core/src/main/java/com/sismics/docs/core/listener/async/FileDeletedAsyncListener.java b/docs-core/src/main/java/com/sismics/docs/core/listener/async/FileDeletedAsyncListener.java index 43b027b7..c32be428 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/listener/async/FileDeletedAsyncListener.java +++ b/docs-core/src/main/java/com/sismics/docs/core/listener/async/FileDeletedAsyncListener.java @@ -1,13 +1,12 @@ package com.sismics.docs.core.listener.async; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import com.google.common.eventbus.Subscribe; import com.sismics.docs.core.dao.lucene.LuceneDao; import com.sismics.docs.core.event.FileDeletedAsyncEvent; import com.sismics.docs.core.model.jpa.File; import com.sismics.docs.core.util.FileUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Listener on file deleted. @@ -24,7 +23,7 @@ public class FileDeletedAsyncListener { * File deleted. * * @param fileDeletedAsyncEvent File deleted event - * @throws Exception + * @throws Exception e */ @Subscribe public void on(final FileDeletedAsyncEvent fileDeletedAsyncEvent) throws Exception { diff --git a/docs-core/src/main/java/com/sismics/docs/core/listener/async/RouteStepValidateAsyncListener.java b/docs-core/src/main/java/com/sismics/docs/core/listener/async/RouteStepValidateAsyncListener.java index cbe55acd..ce8b729b 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/listener/async/RouteStepValidateAsyncListener.java +++ b/docs-core/src/main/java/com/sismics/docs/core/listener/async/RouteStepValidateAsyncListener.java @@ -39,7 +39,7 @@ public class RouteStepValidateAsyncListener { public void run() { final UserDto user = routeStepValidateEvent.getUser(); - // Send the password recovery email + // Send route step validated email Map paramRootMap = new HashMap<>(); paramRootMap.put("user_name", user.getUsername()); paramRootMap.put("document_id", routeStepValidateEvent.getDocument().getId()); diff --git a/docs-core/src/main/java/com/sismics/util/HtmlToPlainText.java b/docs-core/src/main/java/com/sismics/util/HtmlToPlainText.java index 62fcd212..39bce3b5 100644 --- a/docs-core/src/main/java/com/sismics/util/HtmlToPlainText.java +++ b/docs-core/src/main/java/com/sismics/util/HtmlToPlainText.java @@ -1,58 +1,20 @@ package com.sismics.util; -import org.jsoup.Jsoup; import org.jsoup.helper.StringUtil; -import org.jsoup.helper.Validate; -import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.nodes.Node; import org.jsoup.nodes.TextNode; -import org.jsoup.select.Elements; import org.jsoup.select.NodeTraversor; import org.jsoup.select.NodeVisitor; -import java.io.IOException; - /** * HTML to plain-text. This example program demonstrates the use of jsoup to convert HTML input to lightly-formatted * plain-text. That is divergent from the general goal of jsoup's .text() methods, which is to get clean data from a * scrape. - *

              - * Note that this is a fairly simplistic formatter -- for real world use you'll want to embrace and extend. - *

              - *

              - * To invoke from the command line, assuming you've downloaded the jsoup jar to your current directory:

              - *

              java -cp jsoup.jar org.jsoup.examples.HtmlToPlainText url [selector]

              - * where url is the URL to fetch, and selector is an optional CSS selector. - * + * * @author Jonathan Hedley, jonathan@hedley.net */ public class HtmlToPlainText { - private static final String userAgent = "Mozilla/5.0 (jsoup)"; - private static final int timeout = 5 * 1000; - - public static void main(String... args) throws IOException { - Validate.isTrue(args.length == 1 || args.length == 2, "usage: java -cp jsoup.jar org.jsoup.examples.HtmlToPlainText url [selector]"); - final String url = args[0]; - final String selector = args.length == 2 ? args[1] : null; - - // fetch the specified URL and parse to a HTML DOM - Document doc = Jsoup.connect(url).userAgent(userAgent).timeout(timeout).get(); - - HtmlToPlainText formatter = new HtmlToPlainText(); - - if (selector != null) { - Elements elements = doc.select(selector); // get each element that matches the CSS selector - for (Element element : elements) { - String plainText = formatter.getPlainText(element); // format that element to plain text - System.out.println(plainText); - } - } else { // format the whole doc - String plainText = formatter.getPlainText(doc); - System.out.println(plainText); - } - } - /** * Format an Element to plain-text * @param element the root element to format diff --git a/docs-web/src/main/webapp/src/app/docs/controller/document/Document.js b/docs-web/src/main/webapp/src/app/docs/controller/document/Document.js index ba70097f..c81fb75e 100644 --- a/docs-web/src/main/webapp/src/app/docs/controller/document/Document.js +++ b/docs-web/src/main/webapp/src/app/docs/controller/document/Document.js @@ -13,6 +13,7 @@ angular.module('docs').controller('Document', function ($scope, $rootScope, $tim $scope.currentPage = 1; $scope.limit = _.isUndefined(localStorage.documentsPageSize) ? '10' : localStorage.documentsPageSize; $scope.search = $state.params.search ? $state.params.search : ''; + $scope.setSearch = function (search) { $scope.search = search }; $scope.searchOpened = false; $scope.searchDropdownAnchor = angular.element(document.querySelector('.search-dropdown-anchor')); $scope.paginationShown = true; diff --git a/docs-web/src/main/webapp/src/partial/docs/document.view.html b/docs-web/src/main/webapp/src/partial/docs/document.view.html index b309daf5..9a27b326 100644 --- a/docs-web/src/main/webapp/src/partial/docs/document.view.html +++ b/docs-web/src/main/webapp/src/partial/docs/document.view.html @@ -63,7 +63,7 @@
              • - {{ tag.name }} + {{ tag.name }}
            From 5426be9fa019a5c04a3f211c3c045e3a006a8456 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Mon, 12 Mar 2018 14:15:00 +0100 Subject: [PATCH 190/288] Closes #193: last updated date (db + search + ui) --- .../docs/core/dao/jpa/DocumentDao.java | 18 +++++-- .../dao/jpa/criteria/DocumentCriteria.java | 26 ++++++++++ .../docs/core/dao/jpa/dto/DocumentDto.java | 13 +++++ .../sismics/docs/core/model/jpa/Document.java | 19 +++++-- .../src/main/resources/config.properties | 2 +- .../resources/db/update/dbupdate-018-0.sql | 4 ++ docs-web/src/dev/resources/config.properties | 2 +- .../docs/rest/resource/DocumentResource.java | 49 +++++++++++++++---- .../app/docs/controller/document/Document.js | 6 +++ docs-web/src/main/webapp/src/locale/en.json | 7 ++- .../webapp/src/partial/docs/document.html | 34 ++++++++++++- .../src/partial/docs/document.view.html | 2 +- docs-web/src/prod/resources/config.properties | 2 +- .../src/stress/resources/config.properties | 2 +- .../docs/rest/TestAuditLogResource.java | 41 +++++++++++++--- .../docs/rest/TestDocumentResource.java | 6 +++ 16 files changed, 202 insertions(+), 31 deletions(-) create mode 100644 docs-core/src/main/resources/db/update/dbupdate-018-0.sql diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/DocumentDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/DocumentDao.java index c15c9624..f853d531 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/DocumentDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/DocumentDao.java @@ -38,6 +38,7 @@ public class DocumentDao { public String create(Document document, String userId) { // Create the UUID document.setId(UUID.randomUUID().toString()); + document.setUpdateDate(new Date()); // Create the document EntityManager em = ThreadLocalContext.get().getEntityManager(); @@ -90,7 +91,7 @@ public class DocumentDao { } EntityManager em = ThreadLocalContext.get().getEntityManager(); - StringBuilder sb = new StringBuilder("select distinct d.DOC_ID_C, d.DOC_TITLE_C, d.DOC_DESCRIPTION_C, d.DOC_SUBJECT_C, d.DOC_IDENTIFIER_C, d.DOC_PUBLISHER_C, d.DOC_FORMAT_C, d.DOC_SOURCE_C, d.DOC_TYPE_C, d.DOC_COVERAGE_C, d.DOC_RIGHTS_C, d.DOC_CREATEDATE_D, d.DOC_LANGUAGE_C, "); + StringBuilder sb = new StringBuilder("select distinct d.DOC_ID_C, d.DOC_TITLE_C, d.DOC_DESCRIPTION_C, d.DOC_SUBJECT_C, d.DOC_IDENTIFIER_C, d.DOC_PUBLISHER_C, d.DOC_FORMAT_C, d.DOC_SOURCE_C, d.DOC_TYPE_C, d.DOC_COVERAGE_C, d.DOC_RIGHTS_C, d.DOC_CREATEDATE_D, d.DOC_UPDATEDATE_D, d.DOC_LANGUAGE_C, "); sb.append(" (select count(s.SHA_ID_C) from T_SHARE s, T_ACL ac where ac.ACL_SOURCEID_C = d.DOC_ID_C and ac.ACL_TARGETID_C = s.SHA_ID_C and ac.ACL_DELETEDATE_D is null and s.SHA_DELETEDATE_D is null), "); sb.append(" (select count(f.FIL_ID_C) from T_FILE f where f.FIL_DELETEDATE_D is null and f.FIL_IDDOC_C = d.DOC_ID_C), "); sb.append(" u.USE_USERNAME_C "); @@ -122,6 +123,7 @@ public class DocumentDao { documentDto.setCoverage((String) o[i++]); documentDto.setRights((String) o[i++]); documentDto.setCreateTimestamp(((Timestamp) o[i++]).getTime()); + documentDto.setUpdateTimestamp(((Timestamp) o[i++]).getTime()); documentDto.setLanguage((String) o[i++]); documentDto.setShared(((Number) o[i++]).intValue() > 0); documentDto.setFileCount(((Number) o[i++]).intValue()); @@ -204,7 +206,7 @@ public class DocumentDao { StringBuilder sb = new StringBuilder("select distinct d.DOC_ID_C c0, d.DOC_TITLE_C c1, d.DOC_DESCRIPTION_C c2, d.DOC_CREATEDATE_D c3, d.DOC_LANGUAGE_C c4, "); sb.append(" (select count(s.SHA_ID_C) from T_SHARE s, T_ACL ac where ac.ACL_SOURCEID_C = d.DOC_ID_C and ac.ACL_TARGETID_C = s.SHA_ID_C and ac.ACL_DELETEDATE_D is null and s.SHA_DELETEDATE_D is null) c5, "); sb.append(" (select count(f.FIL_ID_C) from T_FILE f where f.FIL_DELETEDATE_D is null and f.FIL_IDDOC_C = d.DOC_ID_C) c6, "); - sb.append(" rs2.RTP_ID_C c7, rs2.RTP_NAME_C "); + sb.append(" rs2.RTP_ID_C c7, rs2.RTP_NAME_C, d.DOC_UPDATEDATE_D c8 "); sb.append(" from T_DOCUMENT d "); sb.append(" left join (select rs.*, rs3.idDocument\n" + "from T_ROUTE_STEP rs \n" + @@ -238,6 +240,14 @@ public class DocumentDao { criteriaList.add("d.DOC_CREATEDATE_D <= :createDateMax"); parameterMap.put("createDateMax", criteria.getCreateDateMax()); } + if (criteria.getUpdateDateMin() != null) { + criteriaList.add("d.DOC_UPDATEDATE_D >= :updateDateMin"); + parameterMap.put("updateDateMin", criteria.getUpdateDateMin()); + } + if (criteria.getUpdateDateMax() != null) { + criteriaList.add("d.DOC_UPDATEDATE_D <= :updateDateMax"); + parameterMap.put("updateDateMax", criteria.getUpdateDateMax()); + } if (criteria.getTagIdList() != null && !criteria.getTagIdList().isEmpty()) { int index = 0; List tagCriteriaList = Lists.newArrayList(); @@ -288,7 +298,8 @@ public class DocumentDao { documentDto.setShared(((Number) o[i++]).intValue() > 0); documentDto.setFileCount(((Number) o[i++]).intValue()); documentDto.setActiveRoute(o[i++] != null); - documentDto.setCurrentStepName((String) o[i]); + documentDto.setCurrentStepName((String) o[i++]); + documentDto.setUpdateTimestamp(((Timestamp) o[i]).getTime()); documentDtoList.add(documentDto); } @@ -323,6 +334,7 @@ public class DocumentDao { documentDb.setRights(document.getRights()); documentDb.setCreateDate(document.getCreateDate()); documentDb.setLanguage(document.getLanguage()); + documentDb.setUpdateDate(new Date()); // Create audit log AuditLogUtil.create(documentDb, AuditLogType.UPDATE, userId); diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/criteria/DocumentCriteria.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/criteria/DocumentCriteria.java index b39cf87f..73de591b 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/criteria/DocumentCriteria.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/criteria/DocumentCriteria.java @@ -35,6 +35,16 @@ public class DocumentCriteria { */ private Date createDateMax; + /** + * Minimum update date. + */ + private Date updateDateMin; + + /** + * Maximum update date. + */ + private Date updateDateMax; + /** * Tag IDs. */ @@ -136,6 +146,22 @@ public class DocumentCriteria { return activeRoute; } + public Date getUpdateDateMin() { + return updateDateMin; + } + + public void setUpdateDateMin(Date updateDateMin) { + this.updateDateMin = updateDateMin; + } + + public Date getUpdateDateMax() { + return updateDateMax; + } + + public void setUpdateDateMax(Date updateDateMax) { + this.updateDateMax = updateDateMax; + } + public DocumentCriteria setActiveRoute(Boolean activeRoute) { this.activeRoute = activeRoute; return this; diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/dto/DocumentDto.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/dto/DocumentDto.java index ef7b4075..39e2dce4 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/dto/DocumentDto.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/dto/DocumentDto.java @@ -71,6 +71,11 @@ public class DocumentDto { */ private Long createTimestamp; + /** + * Update date. + */ + private Long updateTimestamp; + /** * Shared status. */ @@ -236,6 +241,14 @@ public class DocumentDto { return currentStepName; } + public Long getUpdateTimestamp() { + return updateTimestamp; + } + + public void setUpdateTimestamp(Long updateTimestamp) { + this.updateTimestamp = updateTimestamp; + } + public DocumentDto setCurrentStepName(String currentStepName) { this.currentStepName = currentStepName; return this; diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Document.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Document.java index 7b9da6f2..626e631c 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Document.java +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Document.java @@ -1,13 +1,12 @@ package com.sismics.docs.core.model.jpa; -import java.util.Date; +import com.google.common.base.MoreObjects; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Table; - -import com.google.common.base.MoreObjects; +import java.util.Date; /** * Document entity. @@ -102,6 +101,12 @@ public class Document implements Loggable { @Column(name = "DOC_CREATEDATE_D", nullable = false) private Date createDate; + /** + * Creation date. + */ + @Column(name = "DOC_UPDATEDATE_D", nullable = false) + private Date updateDate; + /** * Deletion date. */ @@ -229,6 +234,14 @@ public class Document implements Loggable { this.deleteDate = deleteDate; } + public Date getUpdateDate() { + return updateDate; + } + + public void setUpdateDate(Date updateDate) { + this.updateDate = updateDate; + } + @Override public String toString() { return MoreObjects.toStringHelper(this) diff --git a/docs-core/src/main/resources/config.properties b/docs-core/src/main/resources/config.properties index 3be02720..4e046a45 100644 --- a/docs-core/src/main/resources/config.properties +++ b/docs-core/src/main/resources/config.properties @@ -1 +1 @@ -db.version=17 \ No newline at end of file +db.version=18 \ No newline at end of file diff --git a/docs-core/src/main/resources/db/update/dbupdate-018-0.sql b/docs-core/src/main/resources/db/update/dbupdate-018-0.sql new file mode 100644 index 00000000..17b4dd7b --- /dev/null +++ b/docs-core/src/main/resources/db/update/dbupdate-018-0.sql @@ -0,0 +1,4 @@ +alter table T_DOCUMENT add column DOC_UPDATEDATE_D datetime; +update T_DOCUMENT set DOC_UPDATEDATE_D = DOC_CREATEDATE_D; +alter table T_DOCUMENT alter column DOC_UPDATEDATE_D datetime not null; +update T_CONFIG set CFG_VALUE_C = '18' where CFG_ID_C = 'DB_VERSION'; diff --git a/docs-web/src/dev/resources/config.properties b/docs-web/src/dev/resources/config.properties index 1c41d115..28a3b40a 100644 --- a/docs-web/src/dev/resources/config.properties +++ b/docs-web/src/dev/resources/config.properties @@ -1,3 +1,3 @@ api.current_version=${project.version} api.min_version=1.0 -db.version=17 \ No newline at end of file +db.version=18 \ No newline at end of file diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/DocumentResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/DocumentResource.java index 74593c05..91785ef2 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/DocumentResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/DocumentResource.java @@ -76,6 +76,7 @@ public class DocumentResource extends BaseResource { * @apiSuccess {String} title Title * @apiSuccess {String} description Description * @apiSuccess {Number} create_date Create date (timestamp) + * @apiSuccess {Number} update_date Update date (timestamp) * @apiSuccess {String} language Language * @apiSuccess {Boolean} shared True if the document is shared * @apiSuccess {Number} file_count Number of files in this document @@ -142,6 +143,7 @@ public class DocumentResource extends BaseResource { .add("title", documentDto.getTitle()) .add("description", JsonUtil.nullable(documentDto.getDescription())) .add("create_date", documentDto.getCreateTimestamp()) + .add("update_date", documentDto.getUpdateTimestamp()) .add("language", documentDto.getLanguage()) .add("shared", documentDto.getShared()) .add("file_count", documentDto.getFileCount()); @@ -328,6 +330,7 @@ public class DocumentResource extends BaseResource { * @apiSuccess {String} documents.title Title * @apiSuccess {String} documents.description Description * @apiSuccess {Number} documents.create_date Create date (timestamp) + * @apiSuccess {Number} documents.update_date Update date (timestamp) * @apiSuccess {String} documents.language Language * @apiSuccess {Boolean} documents.shared True if the document is shared * @apiSuccess {Boolean} documents.active_route True if a route is active on this document @@ -394,6 +397,7 @@ public class DocumentResource extends BaseResource { .add("title", documentDto.getTitle()) .add("description", JsonUtil.nullable(documentDto.getDescription())) .add("create_date", documentDto.getCreateTimestamp()) + .add("update_date", documentDto.getUpdateTimestamp()) .add("language", documentDto.getLanguage()) .add("shared", documentDto.getShared()) .add("active_route", documentDto.isActiveRoute()) @@ -464,32 +468,57 @@ public class DocumentResource extends BaseResource { break; case "after": case "before": + case "uafter": + case "ubefore": // New date span criteria try { + boolean isUpdated = params[0].startsWith("u"); DateTime date = formatter.parseDateTime(params[1]); - if (params[0].equals("before")) documentCriteria.setCreateDateMax(date.toDate()); - else documentCriteria.setCreateDateMin(date.toDate()); + if (params[0].endsWith("before")) { + if (isUpdated) documentCriteria.setUpdateDateMax(date.toDate()); + else documentCriteria.setCreateDateMax(date.toDate()); + } else { + if (isUpdated) documentCriteria.setUpdateDateMin(date.toDate()); + else documentCriteria.setCreateDateMin(date.toDate()); + } } catch (IllegalArgumentException e) { // Invalid date, returns no documents - if (params[0].equals("before")) documentCriteria.setCreateDateMax(new Date(0)); - else documentCriteria.setCreateDateMin(new Date(Long.MAX_VALUE / 2)); + documentCriteria.setCreateDateMin(new Date(0)); + documentCriteria.setCreateDateMax(new Date(0)); } break; + case "uat": case "at": // New specific date criteria try { + boolean isUpdated = params[0].startsWith("u"); if (params[1].length() == 10) { DateTime date = dayFormatter.parseDateTime(params[1]); - documentCriteria.setCreateDateMin(date.toDate()); - documentCriteria.setCreateDateMax(date.plusDays(1).minusSeconds(1).toDate()); + if (isUpdated) { + documentCriteria.setUpdateDateMin(date.toDate()); + documentCriteria.setUpdateDateMax(date.plusDays(1).minusSeconds(1).toDate()); + } else { + documentCriteria.setCreateDateMin(date.toDate()); + documentCriteria.setCreateDateMax(date.plusDays(1).minusSeconds(1).toDate()); + } } else if (params[1].length() == 7) { DateTime date = monthFormatter.parseDateTime(params[1]); - documentCriteria.setCreateDateMin(date.toDate()); - documentCriteria.setCreateDateMax(date.plusMonths(1).minusSeconds(1).toDate()); + if (isUpdated) { + documentCriteria.setUpdateDateMin(date.toDate()); + documentCriteria.setUpdateDateMax(date.plusMonths(1).minusSeconds(1).toDate()); + } else { + documentCriteria.setCreateDateMin(date.toDate()); + documentCriteria.setCreateDateMax(date.plusMonths(1).minusSeconds(1).toDate()); + } } else if (params[1].length() == 4) { DateTime date = yearFormatter.parseDateTime(params[1]); - documentCriteria.setCreateDateMin(date.toDate()); - documentCriteria.setCreateDateMax(date.plusYears(1).minusSeconds(1).toDate()); + if (isUpdated) { + documentCriteria.setUpdateDateMin(date.toDate()); + documentCriteria.setUpdateDateMax(date.plusYears(1).minusSeconds(1).toDate()); + } else { + documentCriteria.setCreateDateMin(date.toDate()); + documentCriteria.setCreateDateMax(date.plusYears(1).minusSeconds(1).toDate()); + } } } catch (IllegalArgumentException e) { // Invalid date, returns no documents diff --git a/docs-web/src/main/webapp/src/app/docs/controller/document/Document.js b/docs-web/src/main/webapp/src/app/docs/controller/document/Document.js index c81fb75e..e5e45180 100644 --- a/docs-web/src/main/webapp/src/app/docs/controller/document/Document.js +++ b/docs-web/src/main/webapp/src/app/docs/controller/document/Document.js @@ -181,6 +181,12 @@ angular.module('docs').controller('Document', function ($scope, $rootScope, $tim if (!_.isUndefined($scope.advsearch.before_date)) { search += 'before:' + $filter('date')($scope.advsearch.before_date, 'yyyy-MM-dd') + ' '; } + if (!_.isUndefined($scope.advsearch.after_update_date)) { + search += 'uafter:' + $filter('date')($scope.advsearch.after_update_date, 'yyyy-MM-dd') + ' '; + } + if (!_.isUndefined($scope.advsearch.before_update_date)) { + search += 'ubefore:' + $filter('date')($scope.advsearch.before_update_date, 'yyyy-MM-dd') + ' '; + } if (!_.isEmpty($scope.advsearch.tags)) { search += _.reduce($scope.advsearch.tags, function(s, t) { return s + 'tag:' + t.name + ' '; diff --git a/docs-web/src/main/webapp/src/locale/en.json b/docs-web/src/main/webapp/src/locale/en.json index e6ba5ae4..54e16e84 100644 --- a/docs-web/src/main/webapp/src/locale/en.json +++ b/docs-web/src/main/webapp/src/locale/en.json @@ -45,8 +45,10 @@ "search_fulltext": "Fulltext search", "search_creator": "Creator", "search_language": "Language", - "search_before_date": "Before this date", - "search_after_date": "After this date", + "search_before_date": "Created before this date", + "search_after_date": "Created after this date", + "search_before_update_date": "Updated before this date", + "search_after_update_date": "Update after this date", "search_tags": "Tags", "search_shared": "Only shared documents", "search_workflow": "Workflow assigned to me", @@ -82,6 +84,7 @@ "upgrade_quota": "To upgrade your quota, ask your administrator", "quota": "{{ current | number: 0 }}MB ({{ percent | number: 1 }}%) used on {{ total | number: 0 }}MB", "count": "{{ count }} document{{ count > 1 ? 's' : '' }} found", + "last_updated": "Last updated {{ date | timeAgo: dateFormat }}", "view": { "delete_comment_title": "Delete comment", "delete_comment_message": "Do you really want to delete this comment?", diff --git a/docs-web/src/main/webapp/src/partial/docs/document.html b/docs-web/src/main/webapp/src/partial/docs/document.html index 1a04e204..2ad55053 100644 --- a/docs-web/src/main/webapp/src/partial/docs/document.html +++ b/docs-web/src/main/webapp/src/partial/docs/document.html @@ -93,6 +93,36 @@
            +
            + +
            + +
            +
            + +
            + +
            + +
            +
            +
            @@ -194,7 +224,9 @@ -
            {{ document.create_date | timeAgo: dateFormat }}
            +
            + {{ document.create_date | timeAgo: dateFormat }} +
            diff --git a/docs-web/src/main/webapp/src/partial/docs/document.view.html b/docs-web/src/main/webapp/src/partial/docs/document.view.html index 9a27b326..d4d3905e 100644 --- a/docs-web/src/main/webapp/src/partial/docs/document.view.html +++ b/docs-web/src/main/webapp/src/partial/docs/document.view.html @@ -45,7 +45,7 @@
            +
            +
            + {{ 'settings.workflow.edit.actions' | translate }} +
            +
            +
            +
            + {{ 'workflow_transition.' + transition.name | translate }} +
            +
            +
            +

            {{ 'action_type.' + action.type | translate }}

            +
            +
            + +
            +
            +

            + + {{ 'settings.workflow.edit.remove_action' | translate }} + +

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

            @@ -108,8 +113,9 @@

            - + + diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestRouteResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestRouteResource.java index 36e2fa61..714b5352 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestRouteResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestRouteResource.java @@ -352,14 +352,14 @@ public class TestRouteResource extends BaseJerseyTest { } /** - * Test actions on workflow step. + * Test tag actions on workflow step. */ @Test - public void testAction() { + public void testTagActions() { // Login admin String adminToken = clientUtil.login("admin", "admin", false); - // Create a tag + // Create an Approved tag JsonObject json = target().path("/tag").request() .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) .put(Entity.form(new Form() @@ -367,12 +367,20 @@ public class TestRouteResource extends BaseJerseyTest { .param("color", "#ff0000")), JsonObject.class); String tagApprovedId = json.getString("id"); + // Create a Pending tag + json = target().path("/tag").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) + .put(Entity.form(new Form() + .param("name", "Approved") + .param("color", "#ff0000")), JsonObject.class); + String tagPendingId = json.getString("id"); + // Create a new route model with actions json = target().path("/routemodel").request() .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) .put(Entity.form(new Form() .param("name", "Workflow action 1") - .param("steps", "[{\"type\":\"APPROVE\",\"transitions\":[{\"name\":\"APPROVED\",\"actions\":[{\"type\":\"ADD_TAG\",\"tag\":\"" + tagApprovedId + "\"}]},{\"name\":\"REJECTED\",\"actions\":[]}],\"target\":{\"name\":\"administrators\",\"type\":\"GROUP\"},\"name\":\"Check the document's metadata\"}]")), JsonObject.class); + .param("steps", "[{\"type\":\"APPROVE\",\"transitions\":[{\"name\":\"APPROVED\",\"actions\":[{\"type\":\"ADD_TAG\",\"tag\":\"" + tagApprovedId + "\"}]},{\"name\":\"REJECTED\",\"actions\":[]}],\"target\":{\"name\":\"administrators\",\"type\":\"GROUP\"},\"name\":\"Check the document's metadata\"},{\"type\":\"VALIDATE\",\"transitions\":[{\"name\":\"VALIDATED\",\"actions\":[{\"type\":\"REMOVE_TAG\",\"tag\":\"" + tagPendingId + "\"}]}],\"target\":{\"name\":\"administrators\",\"type\":\"GROUP\"},\"name\":\"Check the document's metadata\"}]")), JsonObject.class); String routeModelId = json.getString("id"); // Create a document @@ -381,6 +389,7 @@ public class TestRouteResource extends BaseJerseyTest { .put(Entity.form(new Form() .param("title", "My super title document 1") .param("description", "My super description for document 1") + .param("tags", tagPendingId) .param("language", "eng")), JsonObject.class); String document1Id = json.getString("id"); @@ -398,7 +407,8 @@ public class TestRouteResource extends BaseJerseyTest { .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) .get(JsonObject.class); JsonArray tags = json.getJsonArray("tags"); - Assert.assertEquals(0, tags.size()); + Assert.assertEquals(1, tags.size()); + Assert.assertEquals(tagPendingId, tags.getJsonObject(0).getString("id")); // Validate the current step with admin target().path("/route/validate").request() @@ -407,6 +417,22 @@ public class TestRouteResource extends BaseJerseyTest { .param("documentId", document1Id) .param("transition", "APPROVED")), JsonObject.class); + // Check tags on document 1 + json = target().path("/document/" + document1Id).request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) + .get(JsonObject.class); + tags = json.getJsonArray("tags"); + Assert.assertEquals(2, tags.size()); + Assert.assertEquals(tagApprovedId, tags.getJsonObject(0).getString("id")); + Assert.assertEquals(tagPendingId, tags.getJsonObject(1).getString("id")); + + // Validate the current step with admin + target().path("/route/validate").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) + .post(Entity.form(new Form() + .param("documentId", document1Id) + .param("transition", "VALIDATED")), JsonObject.class); + // Check tags on document 1 json = target().path("/document/" + document1Id).request() .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) @@ -420,6 +446,7 @@ public class TestRouteResource extends BaseJerseyTest { .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) .put(Entity.form(new Form() .param("title", "My super title document 2") + .param("tags", tagPendingId) .param("language", "eng")), JsonObject.class); String document2Id = json.getString("id"); @@ -439,6 +466,21 @@ public class TestRouteResource extends BaseJerseyTest { .param("documentId", document2Id) .param("transition", "REJECTED")), JsonObject.class); + // Check tags on document 2 + json = target().path("/document/" + document2Id).request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) + .get(JsonObject.class); + tags = json.getJsonArray("tags"); + Assert.assertEquals(1, tags.size()); + Assert.assertEquals(tagPendingId, tags.getJsonObject(0).getString("id")); + + // Validate the current step with admin + target().path("/route/validate").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) + .post(Entity.form(new Form() + .param("documentId", document2Id) + .param("transition", "VALIDATED")), JsonObject.class); + // Check tags on document 2 json = target().path("/document/" + document2Id).request() .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) From b330d54ca28cb804383f53266e0bfa4cf671fd9a Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Tue, 13 Mar 2018 20:07:36 +0100 Subject: [PATCH 194/288] Closes #199: manifest.json --- .../webapp/src/img/icons/icon-128x128.png | Bin 0 -> 9119 bytes .../webapp/src/img/icons/icon-144x144.png | Bin 0 -> 9842 bytes .../webapp/src/img/icons/icon-152x152.png | Bin 0 -> 9604 bytes .../webapp/src/img/icons/icon-192x192.png | Bin 0 -> 10960 bytes .../webapp/src/img/icons/icon-384x384.png | Bin 0 -> 26258 bytes .../webapp/src/img/icons/icon-512x512.png | Bin 0 -> 35468 bytes .../main/webapp/src/img/icons/icon-72x72.png | Bin 0 -> 5453 bytes .../main/webapp/src/img/icons/icon-96x96.png | Bin 0 -> 6995 bytes docs-web/src/main/webapp/src/index.html | 1 + docs-web/src/main/webapp/src/manifest.json | 51 ++++++++++++++++++ 10 files changed, 52 insertions(+) create mode 100644 docs-web/src/main/webapp/src/img/icons/icon-128x128.png create mode 100644 docs-web/src/main/webapp/src/img/icons/icon-144x144.png create mode 100644 docs-web/src/main/webapp/src/img/icons/icon-152x152.png create mode 100644 docs-web/src/main/webapp/src/img/icons/icon-192x192.png create mode 100644 docs-web/src/main/webapp/src/img/icons/icon-384x384.png create mode 100644 docs-web/src/main/webapp/src/img/icons/icon-512x512.png create mode 100644 docs-web/src/main/webapp/src/img/icons/icon-72x72.png create mode 100644 docs-web/src/main/webapp/src/img/icons/icon-96x96.png create mode 100644 docs-web/src/main/webapp/src/manifest.json diff --git a/docs-web/src/main/webapp/src/img/icons/icon-128x128.png b/docs-web/src/main/webapp/src/img/icons/icon-128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..1b62ceb53076fa3fbcf45a6d04155f17a2e4a87f GIT binary patch literal 9119 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

            J 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 literal 0 HcmV?d00001 diff --git a/docs-web/src/main/webapp/src/img/icons/icon-152x152.png b/docs-web/src/main/webapp/src/img/icons/icon-152x152.png new file mode 100644 index 0000000000000000000000000000000000000000..23d5023130ba0f18fb6948112a4f0798edf82a94 GIT binary patch literal 9604 zcmbVybx<7L_TbK62^xaC6D(M82rj|hgN5M1Ay|MV@4fHs z{_(5bs_m-obMNhA_gGh*+wZkBl(5jr&;bAd7D!oM=LM$!y-|^0o+Q0Av@d`ZrT~WN zI$6Oy&0V1Y8A~S%C=JNL+#0F_HMjJ38HS1j00@>gdSDn>T}>3?O$&BM#dBP7blD+&bC{O6*3k>+Y?C8{H@_#as> zGYPtPFqpF_H@By!CzmH5my@eCH;;&j$X_13yqqr>oNnHZFmo?XM>qO^ILJfYAg(sf zFdHXFn!g;)Eu7q85_B(`{(B1!&g$y_W$ftoA49#EjN8lHnVW|T$nD_pcVGX)c7y3a z|F;_dBet8Kw=H$;tko2+(@x1aoqG=j2Qyrz=2XVB=`%f}K4FYAih z{14J)h5lc={x_}V|BuDEUzFkg8yf#dsQk0^5`TY({|)??$$wJ^>iCi`t}lr($B%9c z0MIsp|*@pv2){b?ct<7!DWKBw%ck145;mf4?-dWaQqhn%!2zb50Gpg%*QgJ$16w*KU!!BB4}t1=U)%YIl(PXA{6+05Tpi;Eobd>0x@6Dhkn zs1%e1`rzu?eMAj#*iku)jFd6{nyNu|n~J8E+Cxs&!%Yvj>DIapNIK*BL+(WtbQImg zPdO)G*L`$^VRt0eBr`Go6*YQBC}Vq`G%`@o{nlI@Q^fwq;#l9O6V~zITDssoIIg>9 z@-&UZJ00hQ@mmSZeA{k^_I?M4C!D^(%d|=GP=W)I=e$16pR$6@{RWX=)L1gf$UH(@sfgdgJ4>kce!y@588$aJ0YWTlIPMcctAzr<&vS{Rzb;v7_X-%QGWH zz-Sm6E~_+>nzn_b+vVcdg;XaW|F1`93EeOWM&5I8wb$L6nKMgvU5}yJhld7dbw@fR zGG_{l03v|>G_5Ki640Ln@iqy3CLaG{-F13>^BHp0QrIEoBO?w<1Pbp>tQ{EH-^1Q5 z<`TC+0+-rDY46VVzLNDjAk)4_0T`-nTC@dA;d?vU9d$Kn=KP6YoNs&sz;0YrdeBmY zH?2QB?EK`k{KSr!gG2plyeK5ps=8M}6Yl-^fJzJGRwU&_U@%`lu*m^?zS7W0i z(nb=}=1ldO4_8V7k=sPxssp9pk?Db9ljKm+@J(IXn7G$KY!9npu*EO}6~LaB-7^(F zwQ9et?t~|S>OOaMp&mipw3wCz8!qPkE+Le(#U^5`tu1nXMW4GsjFQkMrXjp1(xlXQu)fy)HLII!EMY~v_fxFK!qYZK)}Ta!`(AH( zIWE-6n3DVlqy8uA&cKINGw|yzrPj(_ffv#K4PyLfd)C>yr+%x)hn)jL>N&39;v5>? z%jV(%Sz71J%-`OW4tX=pr+yK-w~(%8PaNJ@ zcoIUD2p1#*QUR5bAHHuRu^(v~ZsPYCCz6sIZgU9XPF(-Gd>tj6p@o)0up@WfFuSSg zl&(dRy-;=pQW7jsq=ttb>Rw>p?!SNh;O5~F_#@I7{i+3^E%>Z?T8KiT%gA(d^DAy4 z@1fLx_i7I|%a5v;%!0%mBP^uOdhzEAPGgP9!=j+NtTOu+B&ARSId(JW z+!0n3Ac*QRb8WU!-JYF@Q|eO-xBB(wXZJpzq;+z6Zk%Ci++FP5K3#_BgQZ5AtQ$vl zsOE(f{rbjD>(xQ@Qd&{P6^!Yy=jo_vBm-Kjw)S1zU3S1zBjHWCI-mS|cniZZ6xVcj z{c+(waxMr;kM73F2-u$M;}_4sypp5g4YyT}D!hxE+&z0kt!c8q1&j!{Tagfp0!@Jj zu`&@p3odAF-UB_A`QBU$1LYM4$0gnK!~*^l^TAY-A)@BzC2UAEI#rU)c}LJZe3oSB zGHNN97w#x>WlQRD*TrkI7mh_46MpBTnm!6q_H1`)pBY_E2-(<2f_z3FA;G-xW9FmH zaIE$M8xSJ}w;__Y9f2O^GlC?B{o4aOm1?hVMribyc@pP_%KPTRoj!aN;ICMR^DIAG z;)5)^ZQ;60q5YO<^zil2p5rvlULRnA*js>Rssp0oLU$^ImV^-CEuhvW2s2|ui6)LF zW+&5y<&5L@@*dwAmGrn7NnnbQ9gokRb`f2e6R>nOCKtEMhbFEN;h@fJzPQ}H8J;=r zT-QPtpIl8K1D{d^43hqY1jx!&b|XZim=6+5_zK^rs$%ZeKoaoPhP&Y>Vd!yxxG$FU zO9-wI;p_xi#rKS_F`K>FV?0Ao>&DMI3G%e0w5&5?ch16?t`{XfC{QGi`zhTiKvpT? zdy64v;!+xEVtC*N0+|Yuw zwha$E&<>M`udRC5UfUshua__fMRV##Lc?BGzO>JZp{c6>xp~v{DE^p6zi=r`Au?Q;)$v7r?%=B}y+1;?}l((d)Y#^Ly z7_#*Z-{2I*HF6&JKTX~U$6W{RdwBZyQd5o>JYjL*f#B?LgH-+0GslDUtgVR?e{ALJ zhMN5#G{>rNg@FnANP=y==YoEG=tYNxE9u=uD-|#5De?!n^jp$!zeT3kFp%fgIaxBy zDIJu$SXn)VU&YoPk)bn-%ezMUrkvb(`IziWKX^X0mM!n75%KZENM#Y3VPfTF3J*dC z$XZ{UGMLFFuF%S3eARdx4eFPWNs;q)U4(f-7)otc=t~W$kP?R#&BD%aa$?kP&MkS_ zsD|pa@UXX&>DuD9YljECStAOUQREV7Om#nm>o`^u^)%2envJ@_Ix*osej27m%ty<~ z-DzD(k@T>yyDmLzZ#%h0M)?i?w0F2W03$fTk7YJiBiJ2t2NzAB0NkEOA( zo-7=9Ti=+_SN`e@7J&~75+-lu4)C^ljLq$OX=&RGc1Vk}l!@#7jCWhh(vt1SORXs2 z=D(TUMPPm{je+Z-g8<#P2Mu;cMrL<=5SrUKIjJFTu*^MM#S0e7%};b9Qjav|3}KGg z8Sm2~^})1=UQwSFr4zH*c?n7jXe_9?2CT-|&b&tG$zX9JK&SR+4MMY%64g1hQiBcH zEIn#1^*AA=iulNE+rWA2lxlGnsu`O}F$KZK0B%l_VWmuj^w)k|<^2m7nCx}=F!m%< zl;qoI9(a+CH9mCnpn>_VDM^2oY(Qhh{b|ZYv{MV^aTY6|lp!v?>RyJTk><~Iy;OKz zsNOb7T!Goba>e&odgl9@(!HoSSIHaZK#X7f(>0BvR>gj|;>6}j33TP*#by|(K#%W# zgx>Gv`s@5^=2P9m4raqy`~JB^q?|%mZ3wurseoE+RLr=&X1UYRro|3H9wHAe`GMKw z1@I8~@N;G+P{+>hDEt225X_(HPE_aFzcWFKm4Y_9Xjv9jCaXGO zoh9BaTfiRblT~52C|Ohe<|gwwH;1iWv4>@|eTc$?)seHTkQa7rubcdlA+ykS&J5$?@dFh&E?_~k2BX=2^=7>g>OaXVvo)B>y@?~UXwZ9q3MsdZ3 zNJ;6gd2_kVjhDf$eZ-5w+6aX91o8*Zm`Iz%_XQ}|g)Mr*%`vcV2Hz-WZ1Alo5#LdN zXx-~{I$Ujj&T%{c!O1Zyqgmlb9a@STGfo`r!L}UljVpAXyHUU{0Foopt{ELcOE308vo}ZUkzoB%%d*UE=R10B zlA;og%Ul$^y!vg@6Ht$hzMufvmU$#59yroyvo0|DnD{Z7an%GHAg_2WMZ^#8#)xP2 z)BsWOVqu;rb;(y$mKT`x{!jdH8q9B{zPVQj2-J{dHFvzD@_XRZ;Ye-lAW1pGDhN0r zao>yx$#@peT%~9bSBa}#&{lm{>NBSnhg$YqI}&G!OTR{kwKU4rMvD-fJ~Av-ENM|X zL7<)os`#bj%b7Qc5ahDcXWCmh9p-Z6kno0CsL#3LoNVqB#8mb62ijkNJMI;Cd0aZ4@HP_X7-<8?S~VsyOWv&398spAz{ntp=13ZT-d(kq2zS4= zfZ>Cqtpf34WDooyqq9OFQJr#Vq9fg=?KcgB#dqf-oP#1$L}4CseO#KV>vm47cMRZJ z)x9t)GP8-+FNdrL?R@Ghb zmxmc>vB&p!?-Mf=an%|mLrLDN_*!ZYL<>b^8-L~J3((>CradNfL=~dG`t%+&VkWp3 z%glYj{82fDVPnaE)l^hHE^jlN+&+Sxb02nq8cR#CxQCn+yHsT!MJJ5Gm2YLMkv=7O zSRar`bO!vmot+z6WB&7pMTK8U3V~^qF*s@JBuq3>r zR)Ztrw|J_!*QVXu;}O%s*n^!nY#QnCq3UBhJo=ZhhE9R~l5Y20C&IX;V}5yruCZ&S zoUZD;s+_b*%Z)p;fS6ip(&IHE>`=qNfKRhtTW%|vjSoF8yDXvVS`BWPXDJmG%?+G} zPc?VKd?wgCvMxo4a7^!cwBI|>{+w!AeG zkOUum)DnBduYNachZ3TFP6^P-RKz!8vAt?MxhpvCwmzd~N$K}c8Q`SJc%#+5x)3%PYX^M)u~fI! z4we&n+5+kF-W>ORlrQvRl2&YwV<4pDP(a9-Vd%tQ<1PL1(A^S<1t9j9C&dnRJNSY8 zU@?nue;+56T|V+Wtm%;ur+zF}@bod2AZM!i_;%>0D)&zmCPBBac1|&6jG~8_`}31$C?p1!rm8}JAevs&^_vs2jit#m@GkFt$I?$? z|5y!ws+bGly?Rz&12$DlB+hl3^-Lr#pU6k}8w&pn|C9d8sl;}~d$)@M&aN<1E{@2- zA8a1wWgnaOi0cZpFe!|85IG5DK6z0k&0-D_sa9%Q@F0>XCXZIpM^e8QTD!8|Y;gma z4cC+f-~3y=K;=y z`YofCjilo~qu(LWzOPymOT!*Q6kC!$Xz<3G8npFBq;_o8Kh#w#@}VGB*ib6X><$eJ zR$UAQYLlBxAMFEG?fL!o-H8Pc?-la>=;t<}PO7^f{()@whgQyXeiun=iThVcPWp8| z?u(oWeI>zC!>HY7oJO&+709AAlN3k^#+9d0?k%Y7&{~^j6D?f~w(@Phdfhu`oXP{I zy@!deaU;;K%bdMLYdFOkyU|;n;37OO;uIO{s;Gj}P;H6cX~r)Eo7r;#JL3BLe5WMw z4J5hT58qq4!<7`DWJX5C#`#);d@i&Z@}463I#^bs2@_b*4o0&-`p>GJ%q|4Og6(`( zr9C-FX5|*$%eO;PP3$aw+Elc8fiL_^x6e6G?ZWjX(t;PMHBq>}NWwCGz(i4%UV z`iF2?GfI)G{3ht-wu-{eof`j_48Js+K|VwvK>f-+S8^l&M{s?C<2Q10I27)8O*s?u zT&dl;GeNXHuKjwrV#BZ9h59n9Rm-XBNm^->&O4PQ?KoL*)gn&_>6g*b8Camb`@XOG zNUPD_WL@ceNK%SO14m-}t%BQsKNJent^!8qC zbE--RB3W%nl;0iPO%v?*HVvmPAJ=wt#jU*#6mEP|X(esi5F|vLiatg8dA&P+u1e`~ z@JkYah_Q!kiCJUaI~AkaDeH&) z{gey)eY&1Gn3mQb15EcXt7O<;s5>rne=jH1SirJ&Jiy7s-(}`rV zm9fAe?aNno1gSTTMI;8h*KxAb+;9mxbaGoH*%d~d>FOg^5UG_)9o_Q7YYY>}_G{Kg zx`Pog4B(cb_9K@*RQm#dc}#sTf7fN(NIXDQTV$N$+wut+=!9f>j(ndRwD9#QUn=uM z3-rAOk|dIP6OB=Eu;@#1B|+{J$#zvYXGaqm4pTM`qfl+pD(GXqP*n>>rdT?4{ZOO1v@fn|+un5$rOtOkYn`U7 zT#~?)%s=69z}#w>a}N6~H~(D@u+W~w7>hT%tW;Xi%$yl5{9MMejjQa~VXJX1Ef}vz z6&LnX=RMCi1+Atv>*PvncrQucW`jdDQ%9W-dO(qfPO>gHSB}g-^EI91j~52mKy|}x zKsyt&;M2gnnY)TIB|SZ5f|7%&*II*_iL@q$?UeNV1I+>s_O=3hfadc_bPo=UH-v~n^Mf{(2mZGHfTO-58_%kD=J##F>Ei|UBNPP&I*#tW!={;%azY!gYYC8x>SB`>)fIf< zaxpXrX&6-#Do4|vKNFxmOMUi$=JAI=(Z!INErwuK>%m@qIjD}yuOCog?$+*gW1tA% zQs$Z~Dh+MyE<>lYh`+GFWT==bsor}{k0x4uk97{s8|HyKAKK(Vi zQ2F;0YU&J!p0%Q`YO=*Jk>PZIlV+@fdd%p^QKLKgQyMk(ol2<@*jqy+*|2 zw~hwlCtc~4DH9S?LC0*YO%P4#J54L1crm&Q!>1c2LO@2WoOV;TcGLz!_FiLl4yTdw z=-^q<8x%m6;!jZ%1(~X%_JinXSdRq+Bp_zmBr9LA|J#d$)OsNK6Zw8vW~ZPcAI^O8 zr9OeKpp@$i>n+j_6(*`ZJ=eS@02aw+i{T2~XjqQOra zr#yfqOdwTV`i~3)SPDBU2UT>hG=D-)IheJ(+#9=SIim2E<|X5P3ZS;cIbgwC8ha1e z42{YMig6PPFiTHXNCBy2eMFZjnfGu>;hSF1T&CMPFso@)P2FSHp6# zXS_wvEh`)ajAEvVuoaQ74ca!)cvI<`xR%q*=1Bs zUhrx1R=nqW&_%8wc+HOpAuft#3mZVR)Q}zvkCIZFYfqfpwI#^Xq-3rS;q?3S1H`iL zh{8%?*{`3(A0t`-`;dwF(cvX2)2$d0pRF+kDKg_Xe-(moVTw zd%wYU=H;L;RpXsjc@y(4Onl~JZ!P^j98YKVd;*v$FD`0wHnnPWZo86SUScfYU8aESs6DN87xLe|`50M|82 zQPRNU8wtb&BFsxY!@$3t2UgM_;GNq0?itsL)|sZ?*pM+Wf$&b9 zZ{H1b=RQqT6uCbS^7bxU*Pk<*r7Fyqzhs;Qk}Cx&qR- z$jJ~6(M%oMjf_$2G7!?aRh@M}P{a5h^I6!h6WkngpZS|<8=^{jT{cPQ;=H09EhQx~X>G_P=RSdt(s`>OPs35C&_;}goTQkjt85ex{V7#2S_Il#_=k{E_ zqxD;a6&#mA?!#L=;L37Qe%Twt=B?A@ijhxi^&`KRq=lB^Q7+r&Edan*7=~^X7>=h7 zA=+;ur%nv1>k_D+@zRGw%I)FAwawlS(lp!(a@VMTt_TQOAP%X5g_sw=R+()ZG`pM) z?=c&VY?qeNR)3_|`jEr((qp_`sxkco0gVq+#FX^NlEwVKNE{t~n{qMIm3DDq<$!yeJf_emJ z--Jk2*Kv>+I~N;lbUp@#oj!ecbeKPsQrOJBNg4*oV5IaY`zjb=sEp%&kRKhbJSD`w zZ)?RWGe|b9jrQl%APia^f6iURRmP3n^$;neOFiiAPIiJ~g+amWx=n@!F8q;HD z73c&PuJ{o&uQctCEJ3z>P4sg9x;&~0e`$fv%h`gA1lo2hxZ%1 ziKx20-UhBsY2)PPs0qw`%^l_~V>fsTHF|q5@Tt-Lb9UAdnV5$-bDF`EykS?tgk-=R zoJAGsugO;c``n-ydeqiWGMUlu{^ODDLi1+^x7vp+yQ5cWH6E z^e^|m^Ugi@-Scv?`AO!R`F3`8^4si$sjJE3U{YWL0010C1sTo9W9Q$7j{5kix%dX{ z@j&h>tLOUC(aP1s)Y%dsY2j#Q301T=wYJo>G_~-0J7_5a0H9ddYU#P^sj3K@JKA%a z{*~eMw0C+$0{|l8o=&Fbc9yPCGfQh*2T{7CmJT|ot%WF^?sHX`s*|*(jje*Wv*k-~ zH7#>*J98lmI&m?mh^O$Q0((nWQ>dr?8wVF*Pf@ym=oNlE|9hK@4*CxfS36O*xUetI^cV(alwq?vd#~yI}96s`{Ub9bEq9s7IG^d73(LadX1B z?Ct;d>mSlCu9}wrMdQDfcG2>3vgFdVba8ZZHh&xsEBb$fAAR>f8~Q8w$VM3MZ2LGU zrf*~%&E4!R9b6Skd>7ZeNO#P@NcZ5gNv)FgSq9u?b<%t z{TCMYKVpTYoh?mW9i6ot9pC&r0@Q6BT^(I)9G#%jFP}qoZ5=EeJzV|{&OiGqW9e+` zZfPOs>}U`Dhj)c-{~PO40{^dG{}XHR|Kl;PM>1T0L*xGlm4DxQjK9C9{~7p?7ymhR zEFB)_i}T~e;CZ#O2mmmF6lEl}Jm>fQyj-=m61MJs8Z##FGcbS^q@YhbQAfPsr z3043pq8})0x80@sm?*LzFhYy-3QLZ*eoK5-FaGoMZt3P`$)h)6qzJh6YdS((9@io^i3t|? zP?@`T$$ekb&a;Cre#qlGI;EzNsBctV9J25>IJE1V zG@UX!q$J+%5*^(Qq|SNs4GcQPvZKQoGgB~CKGPfP&M~FEe~cvgznV`5O)EzI&@z-8hi|*P#w+!H7(X(lN$;B!M}gEl0oCzPiS67Bkod%D^W2h4w9D;x z)XMU+A)==+1<+S7B#=1eb{!7Xc%l7fRvkHqPu=x(B7M z@fCKYsIU6@5#xd59`MPRf=p;PCpI!V+30uD^v~uoZ=WrJ^uE5cMK{Z^kpn%N$trHO z*2}HXnX9^SVB}I;H0gxKO$TbxT~ox5Wl}J{g2#-x(+~l|-TH9nd~d{lfnN~ktiico zBe|eZ_{G|Z`LWw#-+R0LHlL%(3cVA)hS7_W`&!fB7BA`F@ zvepI%yzlPp(t6N3Rx?Drn0B2bgEE@=ZRx4JkOgaYWlJrG+N4}p?EgseP6~5eGEHpgP}rEJ{B+>OIeSfg$31?^f{zfYSkZi=?XB`DhzVZdWRN0mYS?zZ z>^UW}aP@;O0U|RXhYrePTpR9crRr$7aJGAB$8o<~g&$k+D52;PQeTn=wszLf(MBvf ztsUv{SxfX}Et^^CElwp$)q=#_Y($p+>P?PTvT)b$3LPHe#;wd zTSAfw&NhCXKbbA8U8LHUKIp)}(&Rt8lZ7aJY^A}yz5nU8IhG!emJ-LGs8;-5n*ZGVI4nC-$0n?YOjv|LMHQ8I*B~o`$Fy>G}FKdp;vr ziP*_gR-eEb7AJ3bcspm6S^h?h%0goWJ8T7u4&8zosJSAU#T0`c!K=59)3p(d<20dxFweur zmH!wT1{DdCkRw36LsI~UAw#q!yX+>_nP$#vyPoC(j9F4p&YBn{d!d<7Xp9$_tnUMm zpK<>3n>1aTBwbp!5)8+fqdep~$oY7X-jfsHbYcPK`3NBWf`rHbHoLpq`HS}C=+@Oj z=d3v2UE9`Z5_h?QpMZJd0rb1*VTeK2W-A7%ImMzE)7YU{!=}^w!`c4c5Wy}9H$xFd z7+bA}k2cdt)zBMJ3Na4Vi58974j z9DFwAAHH7Sur<-SJ3bj$!R!q6lXT(>JpizWI90?#8R z`Gif|x`p1}4y#G`kluD72@-@viI$}*u#D3~V(K%i_-FF0Z1Kr4LNaDIPha_a3flw6{6*jEh-0@!L_(K97q{Q3Oq zF~`T%`OKjhd(5c_1Jro1PZ*wcT6?7h@1Y2`)25-AukdnPJVTFhh=hlBzFpu{bMU#t z+T2t%Id2wb1B#Q@N1F4pe)q%WH$FU;BrEyqi7coh*C5{f>e^n*jEUy`>A3K-vDH8v zRDyzz_DDG0VhpqOy#D6XLcRa!-fju5nrxViYM%p<`F%^$Gna+m z$!YoJn{Vj=@UvapPNmpf2jxBMw4TuRDDr@(l!xc&hTc{lBDBGx39Gh_qg2|z^2uh1 znL1q3bKFweuO(i7ygcDatRl#hMHQQZEb+l)+J_Az&J8ucQEv{Y@tVrZ*@6n8pa;KEmO-*OSp3qCIr*q< z1zq)N(oiX^W^O|s;g9T692*&%gqnO>=4$L`v>YG**ooC@;u)x6T{&GSp6I)=5wKY? z9MnlVO)?e~vGc*WA3ScGdLg2URN^cA)do6Zd1rpKQsiT$OhZ>r%#)DK z3t29iKke0+Zc)c16%$GoFYjwU0nMKUYQFSg-7=?r_*}PlaCW(#uhQ|6zRTgpAOD!B zi)^b^K{s$d%0|u9-~XM)UNxQW)`mtxv1D0?wG;e$RO$zW(WVTJ^F#7R3x9>c)Zxdw zXL?@x2ChG9&XIQ)RoG;UW=B(dGv*gzOZZ(QMsJv5FLc*_7xf$40PZ4r-Y3Wq#SyZz zclmJ-Xj?KMahXs#CM56?L{f7&o5wO0E%#5h6z-^3Zgwp)Lw$;oVg&n#RYGS$=6*q( z7)(t;*Hm@FIL~B*fQ-9Mq-$HtxrDro^hM81tSJp@T&{MKFgl%GP)>?Cja&7NTz>?7 z_c`}Xlcmqb^Civn=spXor%#p`ddHjd3Z|Nc>UPrI-I>Nip+2N*nNh*1Lgs)*@m8$N zKMC5&7{v6c_(dTzzP`^W{n)CshdJ>?k0FG4ddm*eM3*LaWb8GpMt93roo3RL{hRy9 z3?jgUo=V$d6E9j49b^bv@6K7pH$@oMdAHQ9l`khdww>6LjA9&c*=P4jE)CO|ymV`~ z-ujUQ#Nhj`W5(Pnx&z}FKI}}I=7Tf5)D;2VcjA-TReHtC=3I)eF43en^Dq&T9Mv0U zaOmF$s}~J#+tnf#=@wvCpi8UMMPgb|o%c^%zpL=&ONL0PS8BGubUmglBT)*->Xf-N zN4a20{SYAM-KH$vmJw=!?jMjqd4NjzCiYu_v>SiTP^O(LC*KKzN%4s0pnbzsZg&hm zKBumY3-{po%noG(@7~JJFH!~&<~iy^Uis#%AOHmQs}{yBdnw4Azw3TwqNOy*Ea$25 zSyIhIdbCUbAexZxOB_YiZ1B*lgJ|bW5qv(pA5OlVL83;|*k2{SGc&|o|K!S!|9m+8 znd7Fe$?G=?JCiSUb|*TXe%XTS6`%CrHWM7hhgvao+UAIx!Ot&~Jm$e-5%u=&DbhJ6 zPeLU@(kuv&n549EkwA8>T}s>Fan#4ULLxOVn?ALoONuHE0TMu8W-xic>ddRqUQ$xP z2JkKlg6^~ytwcYgeI~8-PGaTFYD=_HGj5O1{|M|0252ho+;n(emEE1s*LA3fJlrlT zdU@&|TT zo+!*h-?hE(6JV&;mKGYVHPE(pNSkC(?;GCY@B}NFG|SL4Z#j(s2)B?O>`O(p7Yy(6 zcNP2&XENGu5Z%1&Nv2L#O?*$RnTu!huVhI`K%?KAy`#I|ufh8gqYdQg zmcJgUA%D)E+&1p+Wk<#P0`*dZVcQh@d{gpcrkpMBx+h5VEs(&GVC56xg7G6)A-$QjY$@pMePkSj9r`L3Vl6Fcl1V`GieOzO;9vsP5;rG zIuqm=%6X1fqH?5aCNwlN3&Vb>F~TdMI5%=P8vWA|n27{8CPj?JTnW*(U7!u>8sB3# zlzpj+z={D=xQh6L+@2F4TH}PvNzBuWMx$}`l9p86vjoU*BR;+TsL?0DQvO>D()|$! z-an?|5-tY2N=$6Wtv>5;Q?lmo5gf-=XG#UADgTzOdRMQe5xv8vb5EvtEZ)t>88{mo z+t*(D1);SJx0fB-WzHu%n^UZLDPO>`b1XKDQ_qz56E7zcA^C*K|y; za2zhBOq*H@Gjr_`ml7G~Il8|it|6>i;m^Xj9JvlM@g%^hxsHi1HB5J}w^%U0}&VYQkK$6ayU{OVPFb=C*=vWdb0m>*# z9d+oOWfd zkm9*{@jxjh0ibx|uF>qfHcH}fEe7#ntr;e;A==+^hVK5lS)jXRCPJK=; zSa8;EO0<*NLIw8H^_#Z1a%=O6vOoOj7Ul^=hCzYgehd8eXsBma%^v&wb1jO zL%7>SbB7z7e-aycOs`3Ic<*Z9g9m6(2Y4=}r6HT=_9kOZzbm+0mQ!au_}uX%^4}1V z3UEv$mR~lB*v(2hgw-|aZ$PlA%RJ8ghivUkDE5a!wGp**>x6+M%@Lb9tCM;TChOOf z87moVIdrMe!Opc4k>8Q_0i^7DSsbo%KC1QLI2*gK8UZ!nSi8!brmO2&uBJjY4iYpw z3k~84w<^5-2a7R|2C-}%-J@UiqOy$mv^^o3A7oun=>(Q;_4HolfC!1b$h^a3aJ`Xe z`M01LvuX{aPw>F%Q?#>=w;LrUr7k5G=v* z!zG+ta0n=Sm<$aNUL6Nk8uH?P&N-2!NAp!QU3K(r$yhREu`m1Lz65|f2h?IJpwi~+2Rxpy@Pbnmmb5%#k;rM_^-?OaWqZx5Y)24RKNZCk6DtZN(sL(Xmeq8^|FqsWh?h%L|MTaSsohr8UR}QK z8!0IjYLMsB(pn<6>k`kb;oX4xbl7vk1c-0ESoJ#ojC&e_ShO%Zr;yrVxg(_I%LrUFI-^etGU5WzN#^frZ0Ogi}D%koTjzefvFDH}yiDY=QoqFHe~+L>8j@8IF=w*smuSZKB#V>~X~=C} zmlaBADg+T789@ZC!?x~gB~wirxQ{;*szdr5g8IEo6nP|f z8{&l-50_07fhd+e(H!W>8#rhAF+<>iOVjx6XU&bf{m#@@lVxZofE~wY4ncezaV8Lu zD0;OQ)oFm(jE7o*r~1Q-Na86>A}eFRA%e zDJzUsR-e(4y%?dz(hSm+A-j4~#wwXGpmY^q0*dk6=?$G3=Dgumh|4@=g}Gz6hAGo= z9m+C(s4%=rJMS~#I<+_@8BiYQI?DdZrGVYd&et9#B4$+Rl0YYmseb0t+V@7hVUJ

            #NwdZmmYg{~MPN#*){8&ua#VoAe~QWJhbv2QWCY81Fd z1Yfs*IeF@)cJ@2%#zDBWGYRAP9l0|684Q>fp8vut^;`FwhG*3B92yh4xhi3TBor%y zLiBcdntD!AL=+SUP0Yi)YMFrsS2}XvY@u<(@Ga7wABTkAm~@XsNanBwO~h2)G$&Lv z3bR+e3)}BG?N6qxrdN5J1r_jX#IT;BUuO}pD*;8;$sBocjDWyiGoSmAPtK}XN= zgMU}-ZB!m&o8^5Ayau#0Ll%>hrjbvU^A__Ncbuxl6iTkB_SgeOF(_5QWgj?igY zQJ0vE7Z||@86RL~Q=aBh>w+UCQNMrc?$CXDMU1D)Pkvz9LZ-#Z05J$m2+A&$$W>h< z+R_eZ735DBk<~}kuIX)jAhM_!ueXqX#bWa<$>-Piy0QDV3!9IeH>%CESQaBcBywYE zypTXK9L`kl8&U$~_+VjdUf0uIy)u&0`NzfIW%8(G{&w_NPaiR_aDAuR+VGe8g8F1J zAW_+Ou>#qcPu`B;*O}>0pdU3BCJ9pB2#_ZPm4IV2@1N_~LQcsw)MW)+gLD2?FF|07lqT?0Oc*K4a`Wgi^P^(R$2(iEgl(E^a1 z)C9tt+|NLAnRKGpoRaV?X?=V!hv&SB>lSN*F3D>f@>)1WFcNco`{>>6I({xUDEe9R zYGh#rhU(`vazT?%a*=RSlA7r&#_VSj>X4MwM7ny{C#GrmnY=o)R4?&^XYRyqw~)o2)PICZnxKijj*uv#??_5#WeqNW z&L$We@pfCW5nv11E7_ z3Q`+8Ba8O4KI@#Tt7z2HKGsq&uy2u=0#|RUCuI*U*}qX8{Bbpw2x-x+6uX;kpcs{h zK8rUlI^AtI#@9m?58v=(xysA&&6!it#uQ3|4CxChO0GS{nPw{-5qTOX6-H`&87^%7 zn8zC%m9+wEtrc*mobnZ78Ixv5gfW$}h}nAgWH6TtZNUoRV(6^BujndLfxB_2*6q9y6}c^fo0z{38+mH0X3Z9tLufWA_%c*+LydsN<{tin zpozXxaFIIeH+QSf_X_b_#p0?gZs&tDI2Pz#o~HWNhl(10`WU!PZQ-cIRc#zA#5d4=O%kEJ*#xX0sBTq(}Y@A)72$zY-AXyKA$l zfT<(Lq0c*}J@=I7Z5ct=ucQ+?h^mjE&MP2p+^CcE>%eNIDN|NWfB(XI#AQ>f4N+Je0^? z@(WKq^N`LUNAqgD;WMk1m&T-c;wXbc)ragr7!YBhh;vm6I8Sqk_aXv_mNzh7hfL{Q zt59V6U4em8QkW^xF8=w)42;Kw7V0JjPTEx;7@455c$NDOx!jm)Y+p;74K&2up0d0z zDjYAelQj^Hs5JLQXOfe3eN%zeJcE~gN#325B6aqSx_xaVn8@WpQKP1QNpX#B-6mXb7kmd0Cb&`OL+;1}?g&aoL_Qe+0{PD(;FL(t7{Rp0- zLt~7Kqk#@uI_>eiyce0K4of71FhRvE#LEqQY;lEOPcZTbS>?QLI6MM8s(jC_mQy-0XWz36$xk1R$@~z!C%mE> z#Df308;+oJiqQb|T2dQxWT5$AT6G0gkQ7Cvz4JOpz@)#WDs6$W&?BPI{hfL(iR%;Q zx(t{jN%~q=qz^(R&5P4pj9_3On1`}9hB=5BiMoj^U)WVGq}w$zC8oFJL$I0RivB_e ztUztW{79%e3n?@oKZk-5wL#MJwsn*72BGbUrDe{MWlOC?krxXMlxX}AD5v-PrF-L3 zO+cia*o*?dp@u6#$*X8&kWAHcReTBPbQr)o%<1b~*2!*5kP*L5J=HrQ(F17ow8EE1 z{B~GYu71H1PgH_VH%4oT@GFGs7IzVKs0;QZ&4LJd^!kFY(Y8+m3{J2c7xzQ(qQAZS zXut;BV3mT|^dUku*jWQ5&UL`leVj-c2>!t9zoiiWS2ctMPTwNx^Xb}>0MEUbEUVYA z7tbWsdd{i;gh!lj&fy^5>{8i%6+)9sRq|RDn+d|}&>i0TvhQ;2g-WoJvD@w1-hnJt za=B03kQ94gDDbl@(;{wR$+Ok-Tp51X*8dbuN);qWX#;v(8fcG(^y=PHRJ5PFJyT?T zci~CjM_!fUYo0ko_U^WWM7YfYJXuKx*7$QTh|jhb>7pDPUWvf797m2A(tn z#cVvDD;?b@Vt8rlLr}HFPQ!I!*KF4h?uzZ}JbPBRYHf+w?o_=#6#&~PC1o;mz%3ts z;4|GxvxuBUDyO6SZI95X%tjOoSqyFBEXW$sWyyZ&Z&HVu3cY*#hJtfTYn#9qDyVVq z!}mi|0AQn7DIW)`bG4Apvb>8wfTKgO*X;i2{IC3gD)+WKZj#9EJY~%5t0)x1xW-rd z)ruyqe+*6|zK|wAISCqX;e7OT1joGw9IpH)I4zn0Hy^%!{(`pp=MNuJ&kWQ!zG5U? z+=P=Wz^JtRrk=-c^S9x2I)YMCR#t-}NCzJgk#*2Rf7b@Z!3MWH3qA-60v2)^-2414 z7dZNqL%0ut6%8&DtbZYQEcxcrL7x~eGECHq`$d%c^;7OA*L#E_#ZJ=?S$VK^$E>k|P;yY|d-fp(k7SKke;!cbpovxb%oMQq&k@ z>^oqI#4zs{l1IA|E~6--s)?B&%+EP|up&1d{e?z>iWt#tQC+kRKvxEwa_ zy|VA>Hiie}N6#WO%4T{G4okcnT@wy$~-l!ES zix%*=x_YtOEgSv!sTxPnA&o&@9j6ihv2BE>GCpi>UIs#jo%vzk#(CEjY-%*+{UNi5P#hs6}I_$5t_DFo;#U9m3{i6p9Vo0mH7&WF2o0#H)2opU|sdRxOjAEPl4#n!$ zL&R>cSLiIpI~4s%2 zs#f}a+A#EV;vHqcxxSoMelfWyh9NZBZ>epUpP86aO7H!M%|-x1)7IoWMW^p8+8<0d z0Ivi5Hil^DecS^`g4_6tKxCIF0hbM@uzVY4Zh$WXRo+j0AyIwvcE~R-nG#rg~-&z0C z&-X9!)hGAZ409Npw=n8p6Pl&UAq6R;x=qEK6=XECwsz_t?b3(J_{2GUm1%BO#9E_v zLm;%XvCEp^t9P?zVe~p_1dGtHPz_w>sPP6kk+9^o+A n5grH)4E*0V39FR{EEs@WR+P80QRda(8dF7CHJJ)2M9_Z#VEGj( literal 0 HcmV?d00001 diff --git a/docs-web/src/main/webapp/src/img/icons/icon-384x384.png b/docs-web/src/main/webapp/src/img/icons/icon-384x384.png new file mode 100644 index 0000000000000000000000000000000000000000..46b8ee18ac5613bde2579a4686dcc76def4d94bf GIT binary patch literal 26258 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 literal 0 HcmV?d00001 diff --git a/docs-web/src/main/webapp/src/img/icons/icon-512x512.png b/docs-web/src/main/webapp/src/img/icons/icon-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..37f912ac165f57db7a080a2aa4a750695c6af670 GIT binary patch literal 35468 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 literal 0 HcmV?d00001 diff --git a/docs-web/src/main/webapp/src/img/icons/icon-72x72.png b/docs-web/src/main/webapp/src/img/icons/icon-72x72.png new file mode 100644 index 0000000000000000000000000000000000000000..16cf24aa807436f574def88def8189aa670e610d GIT binary patch literal 5453 zcmbVQXH-+`whf^drAQYfRHcLjFqF_CK>|c60qGEuP^5$sq;~}yRf<#rL1`i#=^dqm zC=gVNRFR^f6s24|=Xl@yamRgQygkO=U)gKUHP`yq+CTPP!>d=$Fz_+}0DvrQR4FIU9`+H(>t^^Xuf#Brgp(?c1)FuRS!K(^ghoPZpPi=y; zi*BGd!6fi11{dgxQ^E_WtASMf;im-d1QHhH@9yT|1NT=I`imESI{&>55d!^XLUL6V z`dgGa+7P5o^d^8{(oirCDh~z8%Sg+}T~<&~kOIj8$x z@3{W5_8}P){zt}twD!RScoHDS1RtWWH}3R29EJaYPjmO*9sM>uRRh1~?Q(i3ST_U_ z=j%@JAfXVdLZ@$}@h*6{mb?NIsik-siIkN`D$2+xL1kb{ItU$kh0AjCFh%)49RCTc zsHLC?RgguTB6VbBkcwLJn%de>B^ek(8zzIi4E=*edH9g99yr1uzb>bK|G{eiS1erH zn}8(|y)i_h+n)+BbS9FBKF&l>khTd7blt@RPxSNoeK>!|iXeEq+$7+2yov6hzp@K= z`48T;6#lQM|Hk6~KN*9Zih=wtjsI0Df3{BR@AvfIg@3yEck2*5PW#3Cv@uxS&}0CB zH4KH&#Q0CITf4YfPx`9uZ4k{iyO1A5jFJ&bmGbXvmqff-l*O~&I-7KPEOzTZtZ|M7 z496eR->Y*D=NyWEs=@cZ_!BRRPuV3Ke%q#o+L(MV?X{(w9_EtQ#h#FtgS`R2Hk63b zvO~VnLIm=}LutfN^U};#=t|aBR*(SC3SYT^q(G-HT{$I*KX*u_phqBhAvPisvUx27 zy5bM2>J-qu04ZJj>bxhjzfk>3(7iDs5pN$y8F?uJW5qZ;$mBUdQ-PkVJ0ryn6w?PO zz_uoLHw1td>41@@VD;D_Xs77DWeJtrp*_~mEneHo1<((pZw7wYnB^<7;?H&6v}bzM z9)7-esJFQO0*9nTyk6xL)fJ7S7xYwa>gOC1mYDE!nFG154Chf=G#0-;%si@7x#TJ$95(*Sm zWf|Z4@NiIae8{0mS=r$U`$z<*7@ z?~~4J$WCSOePsS=v6a)XiUeK0P{Rdlc5P2B@D=q%TmFkjt$bBX_T=YR1D6NPR<Hx-RIg&-rH1Ocl-ElHtfY#w6XUxlbSW4QpJ_3*RpVI9uoBA$zG*_ zW9c1(#O~mFYbUZ~3r47@*=KE}@w{am0nx{VEqGrRtkI~o!-Uh`jJm#9E6>$!?kN9F z;AH1gH+A2PgfEaN`(iauwUWBN1>A)ed=HPm$-}+S6#pRoQ23uXv@|qa{c=B zML1W9Q@2QN;!#RtPQf>P?~fAEUW4$?V3Cgxt-1Vr8eie8Jc5}OB~t71kijK>@*KlF4^Us~h-EO`@eZLZ1^6ngSV zu;RzB#63P2ix_^Bw7E=T8|^^W*ts7{x67nD5e~OT)#Qug+1Wg}jzbqAujc-?T8& zF}qtKtVR{~t5dz>sO=~vPSA44u&p8F*pU2{P$E#o02P-D~$& zO1M?%B+1=G$+qKkaD;++ml?HS)~a_$M<+k(NBISoUo?(?Bt?{x+pfutFfNRdhPhZ~5RkJwng`bLyOkAr><$WViTO>vqX>=t!q2=QWy*%$}% znRR&~`(D@iXvR6KP*QL~Yc7rL;g9J3Ybo!%cJFw7>elA&z{|>LPWl9|G~2sL%L#8m z3?@fn+rN3Y(*;2LSss8jsWlC;LFQ4ZY_YHy*oARX&g5Ghw{s*IMp=vluJt=)xyNOL zDRkYN@&xb!RA}d>Z_Am?b0caRJ#dFL2!+hbGC(jlGKuboAaxxTdn>G@a-*9GPaWA+ zLgGmmWG@UQAhQfKPSk^agR_$73Af%9oF)y?mUoltnV4J%}?l{iGnw z@;;w;HiD8Iy<3G#mgLr2S32wvlmF#FKNobL}>Bikb-p z`BM*TtoOBW^uJ;H_v+zI=2P+1#aE2C@XyOL))>BvrF>pBFQ7fs@WrdNeua_m$$3e3k@eCC z)k+ESF8kd6I;$rriIXH+J!P8ZZe|opGY8@Nejkl*~qoE;E z8VtVfRAC8amylF`y6*AIZ|7~FI(8n)UZZlCmI3%kmT&bH1xN$w+mDEejEbIra3%y5 z<#!Ps9lQ6GNj|mK0yvab!;ItWY6567{gNEwFGN*!XVdu9Y48}lM!GckzN`C8_B^@HwPq-+`<($|upYF7@ zaJI-?8LDP9BK+1^86w^0>1#b7opagqLLZ>Yydi_1`%FhB7PrgaQLyO^aE5@w`Iy?G z+gqHQzfDu-7Ts>h3w{+ePNp9GYVS5YPs#I8uzDX!^#VfpDtLHOFkex#p%1il*}ZKq zox*Z-Zz=dzh97LElWfi!Iy<2mu=1TNPv?-UT55vXHQD8Dt-&CFYYnNQYF#{NPt7IA z^`v=aO7u4Ti~;l9ASU(XOON80ETp5z8SZ{_sT^N_&U9^<1H%}DkVg2e1Rmh?9tcaw z&cfdQPZh2%`r4QntB-x%tb6B7!)Q}|2x`vcSB8kvawWm~jerhC-)*enY0T z3pQ1M&GwV-g=l|@h=@UZ-q2U6GDEke$aJr+7Fs0WD~7X<@3pl2urNpo?C_mrPDs;} zvV$+=Jm**ZO1Y!8q6$rumPAY&(jxIRYwWXbhwFSZXg!4{a~V5c!TP!0i*@g9xCAi7 zrlS|_;#o+263fPb#k5pzCPT*@>PK|^NmizrQ8xSCwd9nb=0_M0%EbP6Rf9YH%%9Bb ze(6tRV$I8+i?bJht+Mv1!0wjSZ3O5^z2IJXO7Hqv=uNHsO*c6E?E+4KesV~EwTk`v z4KT%MHS}lT`1p_A&n96KxeHE3cT-(mDcSFqP5DFVv_iUgmT(FxBWRqd#A175p47uz zBeiY)R=qTbT49#-?Ib9JTnAl2Er@$nz*@68IF|W?PU)xSai@K$Cr2BQhH;Z7qy0#y z_xjTrYPy#C?v`S@VCB$m>D>?am#?=tJoZcwhXLwsROsVR7GIDt6%UwGdUD%>8Nb|v zea-0)mU)g1 z+u6%_LLRB?@dnT4?u#k}J(>MhrL!ng@}!oZDdkrAjmTpLfy+9xN~`0};?`QJtezx+ zjvaPq0EAnOj8#0OW%BE5AM(Tt_dzl#7Nv2!;DoWwX~Un=5^C^ z+JFN@K^9^3K@{EPbX?v8hsLwZwOjl*anyGT}c~;Svu6;Lf5j|Ip zpyvJbghju%BWa1ybZqN180>@|vS%PPh003UufX0%^1rV6DE#PJZ5vRm%F8mos?Sq= z>jA98BxcL$n=Vh0{{=`ml6|`cUj^(sw!5Vkv}YIibpBvY%4Elqtp}ggr}e&s1G$d) z;x8~OD|Z%NC4?V7P(~IsFAUN5EKwev%4xfhAwiM!wu}2f&t8n>$C6fYVr10O%G7*$ zJ@-%e)fEn@jw3!aaK-wS068t%V{viPtSG~yU+k*)aHD%W;AdRe@%Oa-lg(q*+#=XT z*JG(G)#8^rbaKZW!5y@j2Zi*Itq-#guN^F>PfF~E$zE%`ZZ?)1GH~?~Q%c^s`uh@V zMz3Fa+7YsYRZ~cV$kMvlQQ~5st>%k( z@UFRFUHz;2eqOyI<@DunXT=QMPmR1TM;92~imQHZOXxFoHI64N5oXzUc9x5=6S6b$ z4pq#&3&MseIS%t~)3Y?I^JPhL^{i0Z6VcINSlpcAzRmTrG3J9-GUtj~4oJ3gkmy-;Yl_`e4 z=?A~^SO0v;OkI`1wAh7?*Meyy4kAdGE2YzJi%(eOFw*O(xY|w^(O!eGIU-821OurT zEiS}YN9IpC0&7Q6NvW)=15Vrr9Gg*MfZH28bdN?Z4}MO@!CIlj^M?@m(#a_(%tBUGi~;psL{reZ)C zQvv3h+K}`3qjyXxm!03~NV%;}9)B^spM+6I&(mcr{WL6pnnUmW@llkYN6Q!W(CO*z z68Up6$}`8cCmtC)SfDd<(pck$CevjED#U#S9m?~v)QgL+*%P?yn?psP(umx4Nck0P zPa%;N!j80|8z~I0s!i;N`!}q6DAw(r-9IGSMXt48Ez)dAyj?dGllsLm*n>=;HI3xwT zlr^&yM0zpX@e^araqoN{f}7%Awy4IKgDN`72fmEJ-X#zQlf#CqeD*G5YQ(fx?1)Ov zuFodkc6*%j?0L#j9scqYUQ?F-0UW*8Nyo+5jt(osG?c!GT8F`Bz>)!h!|7dJ-7}4( zuzB8IMeynlhrEu>bDRkvztOHZo0>nip95)VWpy&{aU*bi^NTV9S&}6l(5l3$cKSIM z@3|2wGVx9YoRZx85hL*!()Tq~AQPAnf!&!PvleM<53IrOBZ7QmBklaUKtlK~>F#cj2I-O#Bt!+I zF8=5I=iYVKS?8{G_u6}Z^*+y=-`Z<^J3>!KjS%l49smF!)KFJ8xNoigJU9>T-?sq20u!(#90=?WGQ@bU5C_YvlIakmo?l$4bG!yzQZcQ3)`;fL^q`0^n~evp8(^B=$d zlJ@X4fc>`_|5n<=$j=ofU;y)Q@p6aW=fjruZ}5HW{J7x(*(G1%T) z0{~EmYA7oh`7R!s`?;oX`vt8YubP!Eft`V}Y-U!SM0Sem?`=6W$fs%rX%zAKr!@)^ z3Q~36GbE%k+=*P~-sCD7kSQ{waU{l)4Ps%O0%DwBN9#UIavF*MxWoe=KYn>7y{4%u zk<{8^3JeR}01maR9lgA|&e%PtKR#=>PWRx7o~2XNz+?w#0uohmkz|1UDzK92UW7o$ zLZ)0=RF_pZvT+|(t8C9L_IUWwVXE{4VqgH;lmUqlKN4!zNh8ZM*a<=<(9s2(QGGt8 zo#8-R?j>+tdeHj%cHlsn#L;0mQ5y&}Q0ieO!336*Tu!n+Y(Lg`{HkK?71|owXV^Rq zQFu%dahf9DXT;0NEx8rFJG5#58A!vD1dE`iA+4~vBh~k7gi8}OCmk&=5n4-tLlW8w zxO1(yC+lpD@;1_}Q&dX*j2-v+$l<22@c9C3?s;Q`Z^(Nc1>3uJ^Whlaog$TRP|v2eb{<6g5FICpyZD)&TbYK zO0h|4I4*7m-=XZjpO3qBo8RH!&7!lPt$ zPG`$x59EwF(kDxS29xNR9STQeBoH$L=CiLccg|k~WZb-xmJ8$3#FyV$zm?l>+dt2w zE1p^{N9GMb(^-4FG2>Q%RX++~<81%{Oje?{Upn@?r`ZJOC#T_hs5QJ~F@AbieUo7R z$=?tik24}{IUlC^!@xGRZrXdBRr+v6>W_&$$q_1mXzmv~OeBw;YX(q2|=iKhE3RC$a=jZ&WWI&AL8M z5&JgnW@w!>l01(tru|lalp2*ng+4YkrVv3ukE?B@D4ZM#$+?h4BOu36b)fwa73(3> zpPd*|CGt}Dh$;d_b$#g4da88s)l4oJnI94iVYK6U0=%>$gv42)iRln^tugwebr%qo zIHUM*GCX*rDPN&N)a4)E3QjSG=YyIe{7gmF~jClLjy~OvNuSb5J08bfdl#D!!Wa8tr zbAs4>*}UG{$8eoL72n98quVeU5j(p>5sI0(A>)HgOeqo%Kq|Ca&w^k4k|92Czs-G* zo~m5OUCMr1fdg1rZ27V{$HzT=lX1K^dF(J-HGiMR}qnrfp#9vf9kB= zm)Em)bW-T|f*rTup+Y(uq;gq_O?!IICL78^!dXBym(B;aEv zTx5NO`#}^7W|6H)&}CJV#pm#r`5CeR+5PspR*mQ`MlGEW5sFDG&CXv+xT3@2xJ=&f zF|v9;rgw~$Wa<%X1I%d zv;5+E+28`-{e3}R9Qux%g?O;f=iLRZ79~nqIap?f53v$4*Vr^D{JLRKqp9Uin)cxPqN0;{Jn2$ zB-&p}L%#!@6HVs%rbXvb!Lzi*lcLsGtJ_@0T41DU8|WX2c5syw~=zB1lAoGTnQk<>t=sb|4D+c%xXIrN%_e zx`#lGEIrBe6wNb_qJ`bKmhfq+_kvD{EaU0A88~O|A~)A|C7b{H7;(Qk)scIcV6R_} zvWh0?*KyFq!tzh3S$w~H9jX;gKE|>a!B)N+Z~(PEACl`OsY`MX4e*NMVnU+Q=YYRv zF)-)@IfV6UxEEQVda;&IC>kvX9e%&FcVeerxk%qS#qMN~2qKqrJ3MpR>c2xRlWGj@W*vRLblQ2Bd?(bRz;f5=RDJ=sqAALG`UA?v_1u_pyHad1raIfsyDV61 zTsmM+A;0_xiv0OKtY;tnJxdg%{IpmUezr(HaxAk}OB~)?pD< zEzl6_C8jOi2q~N}sS(5D@3U#hF#Ffu4%QB~^wO@GQwx_-CKju@qz0#vZiyMYpnWR> z*g-vK=YK8(PxvRG~S_=ufOPmI)ko0 z$@yM2xPvgrY#w4Q(PW}J{I}hV$=`PAJtJhqf7&Up0z4>nK-D~n4yeqb>Q|!VJt9^x z`1ztdtX`YjhP|8+-2oW9K{~HFp8)qE+)?PiU0W8YtCQnV=J8*zX0h#p6v@MO)3v9t z>T=JpH`n*58#CY|#m}RY^@8AAGhFsGD*2R*%EGwDAELUvlw?-1nt2njDkt?Q8H?ET z9!j9lL$NMOyV*t-(0`Vi?(*+ zpf7aGykvmYRH-JSAKwumU(IiezcAycld8%99Zt#L8`J zw$U+ZwAQpWPAq@?uY|e!7~(9e=fngM?_Ikjojt`#40hJiRp<{e1si@Uyog2;!<^c` zbzsOYwt_~fRKik!d_3!&4PSl|19TcEr5I+$0mD?P=f&(-R6F)ZdT>x6fHqhuz?Q8o z=efz;^Wuj4P+Ybypa9p-D`kEw;jb-sa;Tz~xbqxVPQWzOZ&;iZP)y+Eq#^tobSwP$ z-s_1O1v|9_`5{x>bC!al3}o6D3MWSC?Z8TYaxP|?2&2<8%7Y2bw5*Dfg)@QNI6R%W zWWM3mIt3XFSu@TS9%-SU@GPh|uaCDZTD+(25Wuq(<~MLwR9L-dlUMo(kpDf0z{?P- zCoZ$2BGKeMSiSJ-?btax4#!wj`c11hEZf=)g58O^lH+Cf1Dx z`KJeaWeD3U*O}a66wjM@0tD||7Gt2D?mBtq?`EW*B0ppw1-N)Fb$mraJE9S;{z{pT zuu70ix34QPX%61CW4Do*m{aMjG1`87==Ol3nFYj~7NJD>Y*N9W1ZC9VlAcvp) z0nnKY7^e5~>+iFF`j&uJBP5RsNy~@xV2Z|>zlo$wA$g0xi9wMk>{G9AZdK44Vt%5{ z%PT|256Iz&2N>@LeU}X1xm%8A+*LbRv>MJdhZ3zvs|jh~WCu++^9NM%mFS6MsQQ90LjNs_mp(TV_MI z^-0(u2@~NX4pu@&{cHo=nkJf8Z0W@&32rByJ&WIb0z${U;wsnyJ3l7FaZ~!WKy0Bu z=R|rmYB3G;qE?;_sw@7~A_%4lBTNXPDJSaw{ierhWmH0W+70I+yuw)dW{N7Uo!eG{ zdF?x*OloB8FcKnsesSjnlgTq0t7)}{4R}^2y1CwEYS5WlBrJ)yN1n^C%G4DA8a+F> zFvmn&1N&FO0I#u0>R2NQvz1=|xjNs)NJ)&hc?qo#a?=VvW37T;GMElL2nHzXnLOhN z-pEUpdG%JOPqDo)Q>;B%>an>~1{R*WoQ;BA30+%SVxPUeXokDP7N`Zx4St$)a)3rAePOmog{=CZOGc{ zWlgdOs&b4mN2aCHykX)(i#mmfeHF@gYShe{Knvua-MwT^;taewS_HJuf%X6jBnJn? z5wJ7Ii=2AgCJQBiWb3@ zfc}HK6*mwscnVcRx?hU0jZe-ej}ki3 zJtK9_WTBss=w{k)dih_X_?_EHLULHFL{C8hPmnPTa{_#aBxQN}H7^|JYmR=4=@#u7 z`r;&1r}eTK%;7kt&}@qTWK`NUBNC+$Kqe)l)+cXh zAk8z}b`)mJImEhAKc}|orL7CzP?yuYiZS#JKfj_vj+DV}t)u5t4os4}H=eI4DTT|q z_BhMQY|h`6)$GQWLaXhquWJ(zH!v=-=`UJaB{Js+?fGbfIxjyRuZ(B5{Sx0QKi zh&#>*t8p~0M#4r`aWM&mVIzs%#f>Kp^Wh)14y3Z%Z#WvSPDa)r7s@LPXyHO+Qrw zl@}sG<;FS3@>ZZa_WTHkqdaku?>y&gYiV7DCK$_617a^}zD^su>b3(;x_Jc)5tSpk zZ?@xloZ>$2Z8#n^91dt*ITbzh@2vYw*sYE#+j0jH&blPmPmN~nqFM~iq>7Wl`b&ESld_+3wecehO69t)^` zFdym&|A+^m;JoXy7q@xc!WVKonpN)Haob|-GG5qO-?LiY!-JDyoYsnU92tq>ein9I zY5zsi*dpT>xaq?+wUTTsfaJz$UI1lj7|TMDBBd_BTI*L)?X{#E-9U6yASdNpe4+AC z+mH+>Xi%m5u_PesiBW5({A-x5wb$Gscbg*_fNa)V_(Am=@lHUrisot1k8-uqtpV8( zbYg#c7aC>J7#hAHvnXW-%HiDc+^uLo9=%P644)U#=&?2|0m@U^*({hG%B%0QCKzwI z1%eD0rcR@7cMe{dFb($2)hoF_ne{QBzP1EKYk-}tl9t_nc=089gPRi5^Y`)FpB zT}|=1l$d&8sk?zxnhp!or{}e`@H6rNfBz}TwajlzevG$)IPiS?a54SEhcakCBQ!kQ zH{Wg1SEIuEx7yzZZ9bNclZb)9A2i}0?{-ND+$_Y+(*N(%?yWm48bG&%x!3RZnwdYp OxinOCl*<*ZQ2zx-{I{q8 literal 0 HcmV?d00001 diff --git a/docs-web/src/main/webapp/src/index.html b/docs-web/src/main/webapp/src/index.html index b1f8aa9e..b63cd652 100644 --- a/docs-web/src/main/webapp/src/index.html +++ b/docs-web/src/main/webapp/src/index.html @@ -7,6 +7,7 @@ + diff --git a/docs-web/src/main/webapp/src/manifest.json b/docs-web/src/main/webapp/src/manifest.json new file mode 100644 index 00000000..89880eb4 --- /dev/null +++ b/docs-web/src/main/webapp/src/manifest.json @@ -0,0 +1,51 @@ +{ + "name": "Sismics Docs", + "short_name": "Sismics Docs", + "theme_color": "#2ab2dc", + "background_color": "#ffffff", + "display": "standalone", + "Scope": "/", + "start_url": "/", + "icons": [ + { + "src": "img/icons/icon-72x72.png", + "sizes": "72x72", + "type": "image/png" + }, + { + "src": "img/icons/icon-96x96.png", + "sizes": "96x96", + "type": "image/png" + }, + { + "src": "img/icons/icon-128x128.png", + "sizes": "128x128", + "type": "image/png" + }, + { + "src": "img/icons/icon-144x144.png", + "sizes": "144x144", + "type": "image/png" + }, + { + "src": "img/icons/icon-152x152.png", + "sizes": "152x152", + "type": "image/png" + }, + { + "src": "img/icons/icon-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "img/icons/icon-384x384.png", + "sizes": "384x384", + "type": "image/png" + }, + { + "src": "img/icons/icon-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ] +} \ No newline at end of file From f44b4bb0e0f8acbc7e4cae88d23ad873d6ea35ea Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Tue, 13 Mar 2018 23:32:05 +0100 Subject: [PATCH 195/288] #201: file processing indicator --- .../core/event/FileCreatedAsyncEvent.java | 5 +-- .../async/FileCreatedAsyncListener.java | 2 + .../com/sismics/docs/core/util/FileUtil.java | 37 +++++++++++++++++++ .../docs/rest/resource/FileResource.java | 3 ++ docs-web/src/main/webapp/src/locale/en.json | 3 +- .../partial/docs/document.view.content.html | 5 +++ docs-web/src/main/webapp/src/style/main.less | 9 +++++ 7 files changed, 60 insertions(+), 4 deletions(-) diff --git a/docs-core/src/main/java/com/sismics/docs/core/event/FileCreatedAsyncEvent.java b/docs-core/src/main/java/com/sismics/docs/core/event/FileCreatedAsyncEvent.java index 17b4c460..5dca827e 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/event/FileCreatedAsyncEvent.java +++ b/docs-core/src/main/java/com/sismics/docs/core/event/FileCreatedAsyncEvent.java @@ -1,11 +1,10 @@ package com.sismics.docs.core.event; -import java.io.InputStream; -import java.nio.file.Path; - import com.google.common.base.MoreObjects; import com.sismics.docs.core.model.jpa.File; +import java.nio.file.Path; + /** * New file created event. * diff --git a/docs-core/src/main/java/com/sismics/docs/core/listener/async/FileCreatedAsyncListener.java b/docs-core/src/main/java/com/sismics/docs/core/listener/async/FileCreatedAsyncListener.java index eb5bf27e..59250bf4 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/listener/async/FileCreatedAsyncListener.java +++ b/docs-core/src/main/java/com/sismics/docs/core/listener/async/FileCreatedAsyncListener.java @@ -59,5 +59,7 @@ public class FileCreatedAsyncListener { // Update Lucene index LuceneDao luceneDao = new LuceneDao(); luceneDao.createFile(fileCreatedAsyncEvent.getFile()); + + FileUtil.endProcessingFile(file.getId()); } } diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/FileUtil.java b/docs-core/src/main/java/com/sismics/docs/core/util/FileUtil.java index d821f3c5..9002ecbc 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/util/FileUtil.java +++ b/docs-core/src/main/java/com/sismics/docs/core/util/FileUtil.java @@ -29,6 +29,9 @@ import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; /** * File entity utilities. @@ -40,6 +43,11 @@ public class FileUtil { * Logger. */ private static final Logger log = LoggerFactory.getLogger(FileUtil.class); + + /** + * File ID of files currently being processed. + */ + private static Set processingFileSet = Collections.synchronizedSet(new HashSet()); /** * Extract content from a file. @@ -269,6 +277,7 @@ public class FileUtil { // Raise a new file created event and document updated event if we have a document if (documentId != null) { + startProcessingFile(fileId); FileCreatedAsyncEvent fileCreatedAsyncEvent = new FileCreatedAsyncEvent(); fileCreatedAsyncEvent.setUserId(userId); fileCreatedAsyncEvent.setLanguage(language); @@ -285,4 +294,32 @@ public class FileUtil { return fileId; } + + /** + * Start processing a file. + * + * @param fileId File ID + */ + public static void startProcessingFile(String fileId) { + processingFileSet.add(fileId); + } + + /** + * End processing a file. + * + * @param fileId File ID + */ + public static void endProcessingFile(String fileId) { + processingFileSet.remove(fileId); + } + + /** + * Return true if a file is currently processing. + * + * @param fileId File ID + * @return True if the file is processing + */ + public static boolean isProcessingFile(String fileId) { + return processingFileSet.contains(fileId); + } } diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java index 84637018..21e04536 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java @@ -196,6 +196,7 @@ public class FileResource extends BaseResource { try { java.nio.file.Path storedFile = DirectoryUtil.getStorageDirectory().resolve(id); java.nio.file.Path unencryptedFile = EncryptionUtil.decryptFile(storedFile, user.getPrivateKey()); + FileUtil.startProcessingFile(id); FileCreatedAsyncEvent fileCreatedAsyncEvent = new FileCreatedAsyncEvent(); fileCreatedAsyncEvent.setUserId(principal.getId()); fileCreatedAsyncEvent.setLanguage(documentDto.getLanguage()); @@ -283,6 +284,7 @@ public class FileResource extends BaseResource { * @apiSuccess {String} files.id ID * @apiSuccess {String} files.mimetype MIME type * @apiSuccess {String} files.name File name + * @apiSuccess {String} files.processing True if the file is currently processing * @apiSuccess {String} files.document_id Document ID * @apiSuccess {String} files.create_date Create date (timestamp) * @apiSuccess {String} files.size File size (in bytes) @@ -321,6 +323,7 @@ public class FileResource extends BaseResource { try { files.add(Json.createObjectBuilder() .add("id", fileDb.getId()) + .add("processing", FileUtil.isProcessingFile(fileDb.getId())) .add("name", JsonUtil.nullable(fileDb.getName())) .add("mimetype", fileDb.getMimeType()) .add("document_id", JsonUtil.nullable(fileDb.getDocumentId())) diff --git a/docs-web/src/main/webapp/src/locale/en.json b/docs-web/src/main/webapp/src/locale/en.json index 538a036f..4b037c41 100644 --- a/docs-web/src/main/webapp/src/locale/en.json +++ b/docs-web/src/main/webapp/src/locale/en.json @@ -114,7 +114,8 @@ "upload_error": "Upload error", "upload_error_quota": "Quota reached", "drop_zone": "Drag & drop files here to upload", - "add_files": "Add files" + "add_files": "Add files", + "file_processing_indicator": "This file is being processed. Searching will not be available before it is complete." }, "workflow": { "workflow": "Workflow", diff --git a/docs-web/src/main/webapp/src/partial/docs/document.view.content.html b/docs-web/src/main/webapp/src/partial/docs/document.view.content.html index deb62018..de675d74 100644 --- a/docs-web/src/main/webapp/src/partial/docs/document.view.content.html +++ b/docs-web/src/main/webapp/src/partial/docs/document.view.content.html @@ -45,6 +45,11 @@

            +
            + +
            +
            diff --git a/docs-web/src/main/webapp/src/style/main.less b/docs-web/src/main/webapp/src/style/main.less index b8208372..1b35671d 100644 --- a/docs-web/src/main/webapp/src/style/main.less +++ b/docs-web/src/main/webapp/src/style/main.less @@ -214,6 +214,15 @@ } } +// File processing indicator +.file-processing-indicator { + position: absolute; + z-index: 2; + margin-left: 8px; + margin-top: 8px; + color: #2ab2dc; +} + // File name .file-name { word-wrap: break-word; From 40951e8da0b45334c33db3d6a6e7a121579c3efb Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Tue, 13 Mar 2018 23:37:26 +0100 Subject: [PATCH 196/288] fix tests --- .../sismics/docs/rest/TestRouteResource.java | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestRouteResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestRouteResource.java index 714b5352..a9bf279a 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestRouteResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestRouteResource.java @@ -371,7 +371,7 @@ public class TestRouteResource extends BaseJerseyTest { json = target().path("/tag").request() .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) .put(Entity.form(new Form() - .param("name", "Approved") + .param("name", "Pending") .param("color", "#ff0000")), JsonObject.class); String tagPendingId = json.getString("id"); @@ -487,5 +487,21 @@ public class TestRouteResource extends BaseJerseyTest { .get(JsonObject.class); tags = json.getJsonArray("tags"); Assert.assertEquals(0, tags.size()); + + // Delete the documents + target().path("/document/" + document1Id) + .request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) + .delete(JsonObject.class); + target().path("/document/" + document2Id) + .request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) + .delete(JsonObject.class); + + // Delete the route model + target().path("/routemodel/" + routeModelId) + .request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) + .delete(JsonObject.class); } } \ No newline at end of file From db721a9d103550853e2029bc328ee846162d7c3f Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Wed, 14 Mar 2018 13:45:37 +0100 Subject: [PATCH 197/288] Closes #198: show hierarchy in tag screen --- README.md | 1 - .../webapp/src/app/docs/controller/tag/Tag.js | 18 ++++--- docs-web/src/main/webapp/src/locale/en.json | 1 - .../webapp/src/partial/docs/document.html | 4 ++ .../src/main/webapp/src/partial/docs/tag.html | 38 +++++++-------- .../webapp/src/partial/docs/usergroup.html | 8 ++++ docs-web/src/main/webapp/src/style/main.less | 47 ++++++++++--------- 7 files changed, 67 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 69e020d5..21985694 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,6 @@ [![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) -[![Read the Docs](https://img.shields.io/readthedocs/pip.svg)](https://demo.sismicsdocs.com/apidoc/) Docs is an open source, lightweight document management system for individuals and businesses. diff --git a/docs-web/src/main/webapp/src/app/docs/controller/tag/Tag.js b/docs-web/src/main/webapp/src/app/docs/controller/tag/Tag.js index 33572c9f..b652b868 100644 --- a/docs-web/src/main/webapp/src/app/docs/controller/tag/Tag.js +++ b/docs-web/src/main/webapp/src/app/docs/controller/tag/Tag.js @@ -7,6 +7,7 @@ angular.module('docs').controller('Tag', function($scope, Restangular, $state) { $scope.tag = { name: '', color: '#3a87ad' }; // Retrieve tags + $scope.tags = []; $scope.loadTags = function() { Restangular.one('tag/list').get().then(function(data) { $scope.tags = data.tags; @@ -14,13 +15,6 @@ angular.module('docs').controller('Tag', function($scope, Restangular, $state) { }; $scope.loadTags(); - /** - * Display a tag. - */ - $scope.viewTag = function(id) { - $state.go('tag.edit', { id: id }); - }; - /** * Add a tag. */ @@ -30,4 +24,14 @@ angular.module('docs').controller('Tag', function($scope, Restangular, $state) { $scope.tag = { name: '', color: '#3a87ad' }; }); }; + + /** + * Find children tags. + * @param parent + */ + $scope.getChildrenTags = function(parent) { + return _.filter($scope.tags, function(tag) { + return tag.parent === parent; + }); + }; }); \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/locale/en.json b/docs-web/src/main/webapp/src/locale/en.json index 4b037c41..7bb5dac1 100644 --- a/docs-web/src/main/webapp/src/locale/en.json +++ b/docs-web/src/main/webapp/src/locale/en.json @@ -199,7 +199,6 @@ "tag": { "new_tag": "New tag", "search": "Search", - "edit_tag": "Edit tag", "default": { "title": "Tags", "message_1": "Tags are labels associated to documents.", diff --git a/docs-web/src/main/webapp/src/partial/docs/document.html b/docs-web/src/main/webapp/src/partial/docs/document.html index 2ad55053..7580c0d9 100644 --- a/docs-web/src/main/webapp/src/partial/docs/document.html +++ b/docs-web/src/main/webapp/src/partial/docs/document.html @@ -265,6 +265,10 @@  
            + +
            + +
            diff --git a/docs-web/src/main/webapp/src/partial/docs/tag.html b/docs-web/src/main/webapp/src/partial/docs/tag.html index dc8ae69e..addc048f 100644 --- a/docs-web/src/main/webapp/src/partial/docs/tag.html +++ b/docs-web/src/main/webapp/src/partial/docs/tag.html @@ -1,3 +1,15 @@ + +
            @@ -11,27 +23,13 @@ {{ 'validation.no_space' | translate }} -

            - - -

            +
              +
            • +
            - - - - - - - -
            -   - {{ tag.name }} - - - - -
            +
            + +
            diff --git a/docs-web/src/main/webapp/src/partial/docs/usergroup.html b/docs-web/src/main/webapp/src/partial/docs/usergroup.html index 3e9e0ec8..d3c922db 100644 --- a/docs-web/src/main/webapp/src/partial/docs/usergroup.html +++ b/docs-web/src/main/webapp/src/partial/docs/usergroup.html @@ -16,6 +16,10 @@
        + +
        + +
        @@ -36,6 +40,10 @@
        + +
        + +
        diff --git a/docs-web/src/main/webapp/src/style/main.less b/docs-web/src/main/webapp/src/style/main.less index 1b35671d..e552057d 100644 --- a/docs-web/src/main/webapp/src/style/main.less +++ b/docs-web/src/main/webapp/src/style/main.less @@ -23,14 +23,20 @@ } } -// Tags list -.table-tags { - td { - vertical-align: middle !important; - } +// Tag tree +ul.tag-tree { + list-style-type: none; + padding: 0; - .label { - font-size: 100%; + li { + margin-left: 20px; + margin-top: 8px; + margin-bottom: 8px; + white-space: nowrap; + + .active { + font-weight: 500; + } } } @@ -329,20 +335,6 @@ input[readonly].share-link { margin-top: 20px; } -// Tag tree -.tag-tree-dropdown { - padding-left: 0; - - .tag-tree { - li { - margin-left: 20px; - margin-top: 8px; - margin-bottom: 8px; - white-space: nowrap; - } - } -} - // Advanced search .btn-open-search > * { vertical-align: middle; @@ -668,6 +660,7 @@ input[readonly].share-link { box-shadow: 0 7px 14px 0 rgba(50,50,93,.1), 0 3px 6px 0 rgba(0,0,0,.07); background: none; border-radius: 4px; + overflow: hidden; .well-3d-header { font-size: 20px; @@ -681,6 +674,18 @@ input[readonly].share-link { } } +.well-3d-background { + position: absolute; + top: 60px; + right: 60px; + z-index: -1; + opacity: 0.03; + + .fas { + font-size: 15vw; + } +} + // Cards .card { min-width: 0; From 1e57ee5fb3b2e1d30af7de7df59c04483472f655 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Wed, 14 Mar 2018 14:53:41 +0100 Subject: [PATCH 198/288] clean up --- README.md | 2 +- .../sismics/docs/core/util/LuceneUtil.java | 22 +++++++------- .../util/totp/GoogleAuthenticator.java | 29 +++++-------------- pom.xml | 3 +- 4 files changed, 23 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 21985694..a01c0e65 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ Install with Docker ------------------- 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. +**The default admin password is "admin". Don't forget to change it before going to production.** docker run --rm --name sismics_docs_latest -d -p 8100:8080 -v sismics_docs_latest:/data sismics/docs:latest diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/LuceneUtil.java b/docs-core/src/main/java/com/sismics/docs/core/util/LuceneUtil.java index 2754c533..52f8df67 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/util/LuceneUtil.java +++ b/docs-core/src/main/java/com/sismics/docs/core/util/LuceneUtil.java @@ -1,7 +1,6 @@ package com.sismics.docs.core.util; -import java.io.IOException; - +import com.sismics.docs.core.model.context.AppContext; import org.apache.lucene.analysis.standard.StandardAnalyzer; import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.IndexWriterConfig; @@ -10,7 +9,7 @@ import org.apache.lucene.store.Directory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.sismics.docs.core.model.context.AppContext; +import java.io.IOException; /** * Lucene utils. @@ -26,8 +25,7 @@ public class LuceneUtil { /** * Encapsulate a process into a Lucene context. * - * @param runnable - * @throws IOException + * @param runnable Runnable */ public static void handle(LuceneRunnable runnable) { // Standard analyzer @@ -53,14 +51,18 @@ public class LuceneUtil { } catch (Exception e) { log.error("Error in running index writing transaction", e); try { - indexWriter.rollback(); + if (indexWriter != null) { + indexWriter.rollback(); + } } catch (IOException e1) { log.error("Cannot rollback index writing transaction", e1); } } try { - indexWriter.close(); + if (indexWriter != null) { + indexWriter.close(); + } } catch (IOException e) { log.error("Cannot commit and close IndexWriter", e); } @@ -75,9 +77,9 @@ public class LuceneUtil { /** * Code to run in a Lucene context. * - * @param indexWriter - * @throws Exception + * @param indexWriter Index writer + * @throws Exception e */ - public abstract void run(IndexWriter indexWriter) throws Exception; + void run(IndexWriter indexWriter) throws Exception; } } diff --git a/docs-core/src/main/java/com/sismics/util/totp/GoogleAuthenticator.java b/docs-core/src/main/java/com/sismics/util/totp/GoogleAuthenticator.java index c37147f9..92305303 100644 --- a/docs-core/src/main/java/com/sismics/util/totp/GoogleAuthenticator.java +++ b/docs-core/src/main/java/com/sismics/util/totp/GoogleAuthenticator.java @@ -30,19 +30,14 @@ package com.sismics.util.totp; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Date; -import java.util.List; -import java.util.Random; +import org.apache.commons.codec.binary.Base32; +import org.apache.commons.codec.binary.Base64; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; - -import org.apache.commons.codec.binary.Base32; -import org.apache.commons.codec.binary.Base64; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.*; /** * This class implements the functionality described in RFC 6238 (TOTP: Time @@ -116,7 +111,7 @@ public final class GoogleAuthenticator { /** * Modulus used to truncate the scratch code. */ - public static final int SCRATCH_CODE_MODULUS = (int) Math.pow(10, SCRATCH_CODE_LENGTH); + private static final int SCRATCH_CODE_MODULUS = (int) Math.pow(10, SCRATCH_CODE_LENGTH); /** * Magic number representing an invalid scratch code. @@ -171,14 +166,6 @@ public final class GoogleAuthenticator { config = new GoogleAuthenticatorConfig(); } - public GoogleAuthenticator(GoogleAuthenticatorConfig config) { - if (config == null) { - throw new IllegalArgumentException("Configuration cannot be null."); - } - - this.config = config; - } - /** * @return the random number generator algorithm. * @since 0.5.0 @@ -227,7 +214,7 @@ public final class GoogleAuthenticator { * @return the validation code for the provided key at the specified instant * of time. */ - int calculateCode(byte[] key, long tm) { + private int calculateCode(byte[] key, long tm) { // Allocating an array of bytes to represent the specified instant // of time. byte[] data = new byte[8]; @@ -439,7 +426,7 @@ public final class GoogleAuthenticator { return authorize(secret, verificationCode, new Date().getTime()); } - public boolean authorize(String secret, int verificationCode, long time) throws GoogleAuthenticatorException { + private boolean authorize(String secret, int verificationCode, long time) throws GoogleAuthenticatorException { // Checking user input and failing if the secret key was not provided. if (secret == null) { throw new IllegalArgumentException("Secret cannot be null."); diff --git a/pom.xml b/pom.xml index 0f862c3c..e7a2b538 100644 --- a/pom.xml +++ b/pom.xml @@ -49,6 +49,7 @@ 1.11.2 1.3.0 + 9.2.13.v20150730 9.2.13.v20150730 9.2.13.v20150730 @@ -199,7 +200,7 @@ commons-lang ${commons-lang.commons-lang.version} - + commons-io commons-io From 2a619849f4fd74ef5d732b055877c23a457db948 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Wed, 14 Mar 2018 15:13:09 +0100 Subject: [PATCH 199/288] #182: thumbnail generation asynchronous --- .../core/event/FileCreatedAsyncEvent.java | 16 ----- .../async/FileCreatedAsyncListener.java | 59 ++++++++++++++++--- .../com/sismics/docs/core/util/FileUtil.java | 38 ++---------- .../docs/rest/resource/FileResource.java | 2 - 4 files changed, 56 insertions(+), 59 deletions(-) diff --git a/docs-core/src/main/java/com/sismics/docs/core/event/FileCreatedAsyncEvent.java b/docs-core/src/main/java/com/sismics/docs/core/event/FileCreatedAsyncEvent.java index 5dca827e..d83b5de1 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/event/FileCreatedAsyncEvent.java +++ b/docs-core/src/main/java/com/sismics/docs/core/event/FileCreatedAsyncEvent.java @@ -26,13 +26,6 @@ public class FileCreatedAsyncEvent extends UserEvent { */ private Path unencryptedFile; - /** - * Unencrypted file containing PDF representation - * of the original file. May be null if the PDF conversion is not - * necessary or not possible. - */ - private Path unencryptedPdfFile; - public File getFile() { return file; } @@ -58,15 +51,6 @@ public class FileCreatedAsyncEvent extends UserEvent { return this; } - public Path getUnencryptedPdfFile() { - return unencryptedPdfFile; - } - - public FileCreatedAsyncEvent setUnencryptedPdfFile(Path unencryptedPdfFile) { - this.unencryptedPdfFile = unencryptedPdfFile; - return this; - } - @Override public String toString() { return MoreObjects.toStringHelper(this) diff --git a/docs-core/src/main/java/com/sismics/docs/core/listener/async/FileCreatedAsyncListener.java b/docs-core/src/main/java/com/sismics/docs/core/listener/async/FileCreatedAsyncListener.java index 59250bf4..330f117e 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/listener/async/FileCreatedAsyncListener.java +++ b/docs-core/src/main/java/com/sismics/docs/core/listener/async/FileCreatedAsyncListener.java @@ -2,15 +2,23 @@ package com.sismics.docs.core.listener.async; import com.google.common.eventbus.Subscribe; import com.sismics.docs.core.dao.jpa.FileDao; +import com.sismics.docs.core.dao.jpa.UserDao; import com.sismics.docs.core.dao.lucene.LuceneDao; import com.sismics.docs.core.event.FileCreatedAsyncEvent; import com.sismics.docs.core.model.jpa.File; +import com.sismics.docs.core.model.jpa.User; +import com.sismics.docs.core.util.EncryptionUtil; import com.sismics.docs.core.util.FileUtil; +import com.sismics.docs.core.util.PdfUtil; import com.sismics.docs.core.util.TransactionUtil; +import com.sismics.util.mime.MimeTypeUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.crypto.Cipher; +import java.nio.file.Path; import java.text.MessageFormat; +import java.util.concurrent.atomic.AtomicReference; /** * Listener on file created. @@ -26,22 +34,55 @@ public class FileCreatedAsyncListener { /** * File created. * - * @param fileCreatedAsyncEvent File created event + * @param event File created event */ @Subscribe - public void on(final FileCreatedAsyncEvent fileCreatedAsyncEvent) { + public void on(final FileCreatedAsyncEvent event) { if (log.isInfoEnabled()) { - log.info("File created event: " + fileCreatedAsyncEvent.toString()); + log.info("File created event: " + event.toString()); + } + + // Guess the mime type a second time, for open document format (first detected as simple ZIP file) + final File file = event.getFile(); + file.setMimeType(MimeTypeUtil.guessOpenDocumentFormat(file, event.getUnencryptedFile())); + + // Convert to PDF if necessary (for thumbnail and text extraction) + Path unencryptedPdfFile = null; + try { + unencryptedPdfFile = PdfUtil.convertToPdf(file, event.getUnencryptedFile()); + } catch (Exception e) { + log.error("Unable to convert to PDF", e); + } + + // Get the user from the database + final AtomicReference user = new AtomicReference<>(); + TransactionUtil.handle(new Runnable() { + @Override + public void run() { + UserDao userDao = new UserDao(); + user.set(userDao.getById(event.getUserId())); + } + }); + if (user.get() == null) { + // The user has been deleted meanwhile + return; + } + + // Generate file variations + try { + Cipher cipher = EncryptionUtil.getEncryptionCipher(user.get().getPrivateKey()); + FileUtil.saveVariations(file, event.getUnencryptedFile(), unencryptedPdfFile, cipher); + } catch (Exception e) { + log.error("Unable to generate thumbnails", e); } // Extract text content from the file - final File file = fileCreatedAsyncEvent.getFile(); long startTime = System.currentTimeMillis(); - final String content = FileUtil.extractContent(fileCreatedAsyncEvent.getLanguage(), file, - fileCreatedAsyncEvent.getUnencryptedFile(), fileCreatedAsyncEvent.getUnencryptedPdfFile()); + final String content = FileUtil.extractContent(event.getLanguage(), file, + event.getUnencryptedFile(), unencryptedPdfFile); log.info(MessageFormat.format("File content extracted in {0}ms", System.currentTimeMillis() - startTime)); - - // Store the text content in the database + + // Save the file to database TransactionUtil.handle(new Runnable() { @Override public void run() { @@ -58,7 +99,7 @@ public class FileCreatedAsyncListener { // Update Lucene index LuceneDao luceneDao = new LuceneDao(); - luceneDao.createFile(fileCreatedAsyncEvent.getFile()); + luceneDao.createFile(event.getFile()); FileUtil.endProcessingFile(file.getId()); } diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/FileUtil.java b/docs-core/src/main/java/com/sismics/docs/core/util/FileUtil.java index 9002ecbc..3c77bede 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/util/FileUtil.java +++ b/docs-core/src/main/java/com/sismics/docs/core/util/FileUtil.java @@ -121,29 +121,6 @@ public class FileUtil { return ocrFile(image, language); } - /** - * Save a file on the storage filesystem. - * - * @param unencryptedFile Unencrypted file - * @param unencryptedPdfFile Unencrypted PDF file - * @param file File to save - * @param privateKey Private key used for encryption - */ - public static void save(Path unencryptedFile, Path unencryptedPdfFile, File file, String privateKey) throws Exception { - Cipher cipher = EncryptionUtil.getEncryptionCipher(privateKey); - Path path = DirectoryUtil.getStorageDirectory().resolve(file.getId()); - try (InputStream inputStream = Files.newInputStream(unencryptedFile)) { - Files.copy(new CipherInputStream(inputStream, cipher), path); - } - - // Generate file variations (errors non-blocking) - try { - saveVariations(file, unencryptedFile, unencryptedPdfFile, cipher); - } catch (Exception e) { - log.error("Unable to generate thumbnails", e); - } - } - /** * Generate file variations. * @@ -152,7 +129,7 @@ public class FileUtil { * @param unencryptedPdfFile Unencrypted PDF file * @param cipher Cipher to use for encryption */ - private static void saveVariations(File file, Path unencryptedFile, Path unencryptedPdfFile, Cipher cipher) throws Exception { + public static void saveVariations(File file, Path unencryptedFile, Path unencryptedPdfFile, Cipher cipher) throws Exception { BufferedImage image = null; if (ImageUtil.isImage(file.getMimeType())) { try (InputStream inputStream = Files.newInputStream(unencryptedFile)) { @@ -262,14 +239,12 @@ public class FileUtil { file.setUserId(userId); String fileId = fileDao.create(file, userId); - // Guess the mime type a second time, for open document format (first detected as simple ZIP file) - file.setMimeType(MimeTypeUtil.guessOpenDocumentFormat(file, unencryptedFile)); - - // Convert to PDF if necessary (for thumbnail and text extraction) - java.nio.file.Path unencryptedPdfFile = PdfUtil.convertToPdf(file, unencryptedFile); - // Save the file - FileUtil.save(unencryptedFile, unencryptedPdfFile, file, user.getPrivateKey()); + Cipher cipher = EncryptionUtil.getEncryptionCipher(user.getPrivateKey()); + Path path = DirectoryUtil.getStorageDirectory().resolve(file.getId()); + try (InputStream inputStream = Files.newInputStream(unencryptedFile)) { + Files.copy(new CipherInputStream(inputStream, cipher), path); + } // Update the user quota user.setStorageCurrent(user.getStorageCurrent() + fileSize); @@ -283,7 +258,6 @@ public class FileUtil { fileCreatedAsyncEvent.setLanguage(language); fileCreatedAsyncEvent.setFile(file); fileCreatedAsyncEvent.setUnencryptedFile(unencryptedFile); - fileCreatedAsyncEvent.setUnencryptedPdfFile(unencryptedPdfFile); ThreadLocalContext.get().addAsyncEvent(fileCreatedAsyncEvent); DocumentUpdatedAsyncEvent documentUpdatedAsyncEvent = new DocumentUpdatedAsyncEvent(); diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java index 21e04536..85d24074 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java @@ -17,7 +17,6 @@ import com.sismics.docs.core.model.jpa.User; import com.sismics.docs.core.util.DirectoryUtil; import com.sismics.docs.core.util.EncryptionUtil; import com.sismics.docs.core.util.FileUtil; -import com.sismics.docs.core.util.PdfUtil; import com.sismics.rest.exception.ClientException; import com.sismics.rest.exception.ForbiddenClientException; import com.sismics.rest.exception.ServerException; @@ -202,7 +201,6 @@ public class FileResource extends BaseResource { fileCreatedAsyncEvent.setLanguage(documentDto.getLanguage()); fileCreatedAsyncEvent.setFile(file); fileCreatedAsyncEvent.setUnencryptedFile(unencryptedFile); - fileCreatedAsyncEvent.setUnencryptedPdfFile(PdfUtil.convertToPdf(file, unencryptedFile)); ThreadLocalContext.get().addAsyncEvent(fileCreatedAsyncEvent); DocumentUpdatedAsyncEvent documentUpdatedAsyncEvent = new DocumentUpdatedAsyncEvent(); From 94e18146fd3221061f35bdb2c3fc8f6ae4cf5e62 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Wed, 14 Mar 2018 18:12:19 +0100 Subject: [PATCH 200/288] #182: fix thumbnail for orphan files --- .../async/FileCreatedAsyncListener.java | 10 +++++---- .../com/sismics/docs/core/util/FileUtil.java | 21 +++++++++++-------- .../sismics/docs/rest/TestFileResource.java | 20 ++++++++++++++---- 3 files changed, 34 insertions(+), 17 deletions(-) diff --git a/docs-core/src/main/java/com/sismics/docs/core/listener/async/FileCreatedAsyncListener.java b/docs-core/src/main/java/com/sismics/docs/core/listener/async/FileCreatedAsyncListener.java index 330f117e..d90c3b15 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/listener/async/FileCreatedAsyncListener.java +++ b/docs-core/src/main/java/com/sismics/docs/core/listener/async/FileCreatedAsyncListener.java @@ -96,10 +96,12 @@ public class FileCreatedAsyncListener { fileDao.update(file); } }); - - // Update Lucene index - LuceneDao luceneDao = new LuceneDao(); - luceneDao.createFile(event.getFile()); + + if (file.getDocumentId() != null) { + // Update Lucene index + LuceneDao luceneDao = new LuceneDao(); + luceneDao.createFile(event.getFile()); + } FileUtil.endProcessingFile(file.getId()); } diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/FileUtil.java b/docs-core/src/main/java/com/sismics/docs/core/util/FileUtil.java index 3c77bede..6ede2e45 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/util/FileUtil.java +++ b/docs-core/src/main/java/com/sismics/docs/core/util/FileUtil.java @@ -60,7 +60,10 @@ public class FileUtil { */ public static String extractContent(String language, File file, Path unencryptedFile, Path unencryptedPdfFile) { String content = null; - + if (language == null) { + return null; + } + if (ImageUtil.isImage(file.getMimeType())) { content = ocrFile(unencryptedFile, language); } else if (VideoUtil.isVideo(file.getMimeType())) { @@ -251,15 +254,15 @@ public class FileUtil { userDao.updateQuota(user); // Raise a new file created event and document updated event if we have a document - if (documentId != null) { - startProcessingFile(fileId); - FileCreatedAsyncEvent fileCreatedAsyncEvent = new FileCreatedAsyncEvent(); - fileCreatedAsyncEvent.setUserId(userId); - fileCreatedAsyncEvent.setLanguage(language); - fileCreatedAsyncEvent.setFile(file); - fileCreatedAsyncEvent.setUnencryptedFile(unencryptedFile); - ThreadLocalContext.get().addAsyncEvent(fileCreatedAsyncEvent); + startProcessingFile(fileId); + FileCreatedAsyncEvent fileCreatedAsyncEvent = new FileCreatedAsyncEvent(); + fileCreatedAsyncEvent.setUserId(userId); + fileCreatedAsyncEvent.setLanguage(language); + fileCreatedAsyncEvent.setFile(file); + fileCreatedAsyncEvent.setUnencryptedFile(unencryptedFile); + ThreadLocalContext.get().addAsyncEvent(fileCreatedAsyncEvent); + if (documentId != null) { DocumentUpdatedAsyncEvent documentUpdatedAsyncEvent = new DocumentUpdatedAsyncEvent(); documentUpdatedAsyncEvent.setUserId(userId); documentUpdatedAsyncEvent.setDocumentId(documentId); diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestFileResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestFileResource.java index 7939e044..102391fd 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestFileResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestFileResource.java @@ -262,21 +262,33 @@ public class TestFileResource extends BaseJerseyTest { Assert.assertNotNull(file1Id); } } - + // Get all orphan files JsonObject json = target().path("/file/list").request() .cookie(TokenBasedSecurityFilter.COOKIE_NAME, file3Token) .get(JsonObject.class); JsonArray files = json.getJsonArray("files"); Assert.assertEquals(1, files.size()); - - // Get the file data - Response response = target().path("/file/" + file1Id + "/data").request() + + // Get the thumbnail data + Response response = target().path("/file/" + file1Id + "/data") + .queryParam("size", "thumb") + .request() .cookie(TokenBasedSecurityFilter.COOKIE_NAME, file3Token) .get(); + Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); InputStream is = (InputStream) response.getEntity(); byte[] fileBytes = ByteStreams.toByteArray(is); Assert.assertEquals(MimeType.IMAGE_JPEG, MimeTypeUtil.guessMimeType(fileBytes, null)); + Assert.assertTrue(fileBytes.length > 0); + + // Get the file data + response = target().path("/file/" + file1Id + "/data").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, file3Token) + .get(); + is = (InputStream) response.getEntity(); + fileBytes = ByteStreams.toByteArray(is); + Assert.assertEquals(MimeType.IMAGE_JPEG, MimeTypeUtil.guessMimeType(fileBytes, null)); Assert.assertEquals(163510, fileBytes.length); // Create a document From dcb924abac10b2da53f91c176410c36ee610d6b8 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Wed, 14 Mar 2018 18:31:06 +0100 Subject: [PATCH 201/288] #182: fix tests --- .../test/java/com/sismics/docs/core/util/TestFileUtil.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs-core/src/test/java/com/sismics/docs/core/util/TestFileUtil.java b/docs-core/src/test/java/com/sismics/docs/core/util/TestFileUtil.java index 9d3ca268..c62561df 100644 --- a/docs-core/src/test/java/com/sismics/docs/core/util/TestFileUtil.java +++ b/docs-core/src/test/java/com/sismics/docs/core/util/TestFileUtil.java @@ -28,7 +28,7 @@ public class TestFileUtil { File file = new File(); file.setMimeType(MimeType.OPEN_DOCUMENT_TEXT); Path pdfPath = PdfUtil.convertToPdf(file, path); - String content = FileUtil.extractContent(null, file, path, pdfPath); + String content = FileUtil.extractContent("eng", file, path, pdfPath); Assert.assertTrue(content.contains("Lorem ipsum dolor sit amen.")); } @@ -38,7 +38,7 @@ public class TestFileUtil { File file = new File(); file.setMimeType(MimeType.OFFICE_DOCUMENT); Path pdfPath = PdfUtil.convertToPdf(file, path); - String content = FileUtil.extractContent(null, file, path, pdfPath); + String content = FileUtil.extractContent("eng", file, path, pdfPath); Assert.assertTrue(content.contains("Lorem ipsum dolor sit amen.")); } @@ -47,7 +47,7 @@ public class TestFileUtil { Path path = Paths.get(ClassLoader.getSystemResource("file/udhr.pdf").toURI()); File file = new File(); file.setMimeType(MimeType.APPLICATION_PDF); - String content = FileUtil.extractContent(null, file, path, path); + String content = FileUtil.extractContent("eng", file, path, path); Assert.assertTrue(content.contains("All human beings are born free and equal in dignity and rights.")); } From 2ac10e81278becf09c97dce3375f7430e193d0df Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Wed, 14 Mar 2018 19:01:28 +0100 Subject: [PATCH 202/288] #182: do not cache the temporary thumbnail --- .../docs/rest/resource/FileResource.java | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java index 85d24074..bc2b2a60 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java @@ -525,12 +525,19 @@ public class FileResource extends BaseResource { return Response.status(Status.SERVICE_UNAVAILABLE).build(); } - return Response.ok(stream) + Response.ResponseBuilder builder = Response.ok(stream) .header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=" + file.getFullName("data")) - .header(HttpHeaders.CONTENT_TYPE, mimeType) - .header(HttpHeaders.CACHE_CONTROL, "private") - .header(HttpHeaders.EXPIRES, HttpUtil.buildExpiresHeader(3_600_000L * 24L * 365L)) - .build(); + .header(HttpHeaders.CONTENT_TYPE, mimeType); + if (decrypt) { + // Cache real files + builder.header(HttpHeaders.CACHE_CONTROL, "private") + .header(HttpHeaders.EXPIRES, HttpUtil.buildExpiresHeader(3_600_000L * 24L * 365L)); + } else { + // Do not cache the temporary thumbnail + builder.header(HttpHeaders.CACHE_CONTROL, "no-store, must-revalidate") + .header(HttpHeaders.EXPIRES, "0"); + } + return builder.build(); } /** From 16215dde3bb651737ed092a08f72bd6433abe02d Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Wed, 14 Mar 2018 20:39:32 +0100 Subject: [PATCH 203/288] #207: new temporary thumbnail --- .../sismics/docs/rest/resource/FileResource.java | 2 +- docs-web/src/main/resources/image/file-thumb.png | Bin 0 -> 751 bytes docs-web/src/main/resources/image/file-web.png | Bin 0 -> 4992 bytes docs-web/src/main/resources/image/file.png | Bin 3457 -> 0 bytes 4 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 docs-web/src/main/resources/image/file-thumb.png create mode 100644 docs-web/src/main/resources/image/file-web.png delete mode 100644 docs-web/src/main/resources/image/file.png diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java index bc2b2a60..993b4225 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java @@ -481,7 +481,7 @@ public class FileResource extends BaseResource { decrypt = true; // Thumbnails are encrypted if (!Files.exists(storedFile)) { try { - storedFile = Paths.get(getClass().getResource("/image/file.png").toURI()); + storedFile = Paths.get(getClass().getResource("/image/file-" + size + ".png").toURI()); } catch (URISyntaxException e) { // Ignore } diff --git a/docs-web/src/main/resources/image/file-thumb.png b/docs-web/src/main/resources/image/file-thumb.png new file mode 100644 index 0000000000000000000000000000000000000000..a1efce0d4be7d4dea0d41b7da94e3e5371d4ca25 GIT binary patch literal 751 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K58911MRQ8&P5Fk|-;1l9%V&!0N>uhQ7;^q_R z=^qjp9upcBA0Cq!6`vBFkeZQGm|Iju3Wu#{l@J(cke%V`0&x=r_Y|f|M21C$4{R=fBy32D-eA9{{1Hq z{QC9#_wT=d{{g}O|Nld?O>Gz$m~=c{978H@y}c716ly5Yc5#8mj*m?%?ud%soOh)l=uw~yzDWL1A03H<9-`k!ZBLCoD>1~q(?_#By3pTFiEV#+W zsH2|k*}#98;nqcG*M`G|3~%2$J2sdJBrI|WYN%w2WL(C%gms4M1o4DP4oMA`Ofwmm zaV%k*p*TT0p~<1Bp`!5(!$YQ*jEh)5F|%-sFm#BnU=Yv>U{VNLz~->PL9oH1QHvpw zDV1Rn%PR&JP7$UK!4)h5N&#F7P7A~w8XOE8J~CZpXycy3oZDZhu&cD;^;%|~X!eZf(d-qHjXn&yJL9(Ve)vC^i3-7jW`x0;ORHxx%ok>o zInVNAn!tn8J`J~97|Mme@4u#$Pv={A3TeRd?7QnRe#?kJag{FKYk( tRNisF{`kHhOYKVhgwaeTkGp~WFZ0n{bNAXy`<8)B@O1TaS?83{1OP9|@*e;I literal 0 HcmV?d00001 diff --git a/docs-web/src/main/resources/image/file-web.png b/docs-web/src/main/resources/image/file-web.png new file mode 100644 index 0000000000000000000000000000000000000000..fb25fce1bf30f7f4c0a24e3e15a55330a8bcdec4 GIT binary patch literal 4992 zcmeHLX;c$g7OtvPn2?sSFs{%}$JS=((Syili;h%e5hQ3p1XP5efJ9bhkyQ(DV?@NU zSw*0sMF@%vNDvtyfJj6^*)fPgKtLivMZ`vsdBxb%J!d?g=_Wt=%$fV+-FI)@_ucO; zbzhx|;%wclL|36h5Ts;gYGegLRM4eDGzw6w_2<9Zud>kN$aagz<3 zHg7aEGcew=$<)HgY@3NW$AoQZYO&pHo3%N|#=^>W>-L?T9lI=T?X7I>t#>-u+B@xX zbl%N%+2iEuu+NR_%yV)%uy?<^vzw=@ySJOiL7vwkcW*xrA3ty3!w3C?eGiBD1%~SHGA^cPre$18&&eRV7Zw#<7ZeI^To;rS z6_*HZ+$=4t_p43JjdFV?#f=VFsRws?i>d*_G>g&p!ORZO;IeBO6xV6Ler1cC11Lf5zl$j5?zoo z>SS1KlQrtJ(bKEMztNx7={@k>h;;0Ww@D#sz5z!neCcD$p}i_R1czW0n{_jLgnv?) zHIW$r3}-+HjY-V9nLWY>3UXqT49RDq<*?%)7J4?Phd64ZQ>l5Q?eUQ}z}nkU~*mqXLsyGu-Jy z!GwuN-nEd{*i``-!5MKZVx`8|rUhCrLF}}Cm+Tq4@||P@4@L8Jf(;5;4fescW=zCR zMJY^ABr)r*D>T8Old{)y^+ex&<{h>lMSVnqBxc%1$B~w$@aGx6GNNxsbKPk&%DGhW zP;Tz~L}g64Y;c`(_>Uz0^ zhPXwr)_FxdT3@gWranL;54E5_v}X{co2M0yd`YnOlzCC8Mo{Fd+eT^%ABZ1 z>8BuliV>_VbsQem2=3pU!$k|6?W}N%CQVf6NKY2tL5ctyWj~T zTHl}7!|+o>ltm!Wa?c@U`#%{ou$sYk4+5GeAk>6kX@Uy=$=b`=LIFi5iZ4NFOv@=A zg2rYCv%%=rAO@2#@^2vB7xiGo)OzVmTjc7k%2K6Ab17eaK=B(SHkcNeK*qo zCW-VnY}W(82Tm#i|ACj5I2xN@u1Nyb*OK&FH$aSF7*N`{pQ*Z z5`m#aBJdAhCZWYe?FxX4r`L57Ln$vp8SinRmNFusc{aVJ8|R(^SW#<3|A>Ntvf|<( zEd+#xIE?eT09WIhgLFybq^&XIg+b$^4Iy`iT>)~AOY65ckH-bzkYbLnw5@Yt3wtbK zDHV}E?_-ks;QGLNeG>cV+9VZXOrA=^e*g*2RKIHM;LEb|0VS{4-OIPbp z*Rs66sD476O;0{-0E%1=&u@#3+>Wwb)<$k@e%*`J?M-FSxQFU2@T_R9a7b-`ilXS- zMm0>>7p)67KvYdW9TaF}aVyG*{mK}gFGMYL;hkF5D3X}In~le_*Mvh8(3hI*?&GPS%Hgpw#CnY@_p_llgEOAx^eb@WNFi-VeKFGTCYzRY%7t+*DP&Kbcj%e#K_xO)`FbQ7H zP7g|xpEOes6TaTiQ$l>7?Xk0xh9ss|X{A+uStfb>G|mJQ>MTeIF;wFPHQX&hHC^`E zM6W?|0)yiLuxGF1%QKi2Comvf8PyDnTWz6@n5`@yEYm>}6`!t@=^%HK-xe&YjH^<= zuk&@K<@bWXD*6hy_oK3DIyc~6%D8#j0@J#tOB!9Mrn%ZG8Lt?LN}P%?m{#PrX+#dR z2^_U3)pPhj#o+ezoGrJ2!t=Aye9ksG*6K;pKq(Yc*l&X0*c<$QN z>qs@XsjP@`m9sZB%WkhrQBhgN9}L7!id)=z9<9?3dr`B5>ov#kuz$eVnEi-qKGpCW z*4L|7tFA5KH%X>a;C`vl%F@!4D*9&gLixP2@~QP0E+01kuVvo)^Ze4I~ V!KBsLQSfICGBe(4l(&f+@edx^z25); literal 0 HcmV?d00001 diff --git a/docs-web/src/main/resources/image/file.png b/docs-web/src/main/resources/image/file.png deleted file mode 100644 index 09912855153e102f97f6fb9680aeeb07de2920bb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3457 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 From bf37c5cb5134325e10ccc95f068f7daf5a6b37b9 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Thu, 15 Mar 2018 11:55:03 +0100 Subject: [PATCH 204/288] #176: Default tag when creating a document with a tag opened --- .../webapp/src/app/docs/controller/document/DocumentEdit.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentEdit.js b/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentEdit.js index e8d366a6..78dae5ff 100644 --- a/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentEdit.js +++ b/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentEdit.js @@ -62,6 +62,10 @@ angular.module('docs').controller('DocumentEdit', function($rootScope, $scope, $ language: language }; + if ($scope.navigatedTag) { + $scope.document.tags.push($scope.navigatedTag); + } + $scope.newFiles = []; if ($scope.form) { From b0bceefc0e83b205f7b5d295fa0f5e1d72845c47 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Thu, 15 Mar 2018 12:28:55 +0100 Subject: [PATCH 205/288] Closes #174, closes #176: add a tag from the navigation --- .../app/docs/controller/document/Document.js | 23 +++++++++++++++++++ .../document/DocumentModalAddTag.js | 11 +++++++++ .../webapp/src/app/docs/controller/tag/Tag.js | 2 +- docs-web/src/main/webapp/src/index.html | 1 + .../src/partial/docs/document.add.tag.html | 19 +++++++++++++++ .../webapp/src/partial/docs/document.html | 10 ++++++++ .../src/main/webapp/src/partial/docs/tag.html | 2 +- .../src/main/webapp/src/style/colorpicker.css | 1 + docs-web/src/main/webapp/src/style/main.less | 10 ++++++++ 9 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 docs-web/src/main/webapp/src/app/docs/controller/document/DocumentModalAddTag.js create mode 100644 docs-web/src/main/webapp/src/partial/docs/document.add.tag.html diff --git a/docs-web/src/main/webapp/src/app/docs/controller/document/Document.js b/docs-web/src/main/webapp/src/app/docs/controller/document/Document.js index e5e45180..4276a661 100644 --- a/docs-web/src/main/webapp/src/app/docs/controller/document/Document.js +++ b/docs-web/src/main/webapp/src/app/docs/controller/document/Document.js @@ -338,4 +338,27 @@ angular.module('docs').controller('Document', function ($scope, $rootScope, $tim return _.pluck(children, 'name').join(', ') + (tag.children.length > 2 ? '...' : ''); }; + + /** + * Add a tag in the current navigation context. + */ + $scope.addTagHere = function () { + $uibModal.open({ + templateUrl: 'partial/docs/document.add.tag.html', + controller: 'DocumentModalAddTag' + }).result.then(function (tag) { + if (tag === null) { + return; + } + + // Create the tag + tag.parent = $scope.navigatedTag ? $scope.navigatedTag.id : undefined; + Restangular.one('tag').put(tag).then(function (data) { + // Add the new tag to the list + tag.id = data.id; + tag.children = []; + $scope.tags.push(tag); + }) + }); + }; }); \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentModalAddTag.js b/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentModalAddTag.js new file mode 100644 index 00000000..e78d0c8f --- /dev/null +++ b/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentModalAddTag.js @@ -0,0 +1,11 @@ +'use strict'; + +/** + * Document modal add tag controller. + */ +angular.module('docs').controller('DocumentModalAddTag', function ($scope, $uibModalInstance) { + $scope.tag = { name: '', color: '#3a87ad' }; + $scope.close = function(tag) { + $uibModalInstance.close(tag); + } +}); \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/app/docs/controller/tag/Tag.js b/docs-web/src/main/webapp/src/app/docs/controller/tag/Tag.js index b652b868..86338a63 100644 --- a/docs-web/src/main/webapp/src/app/docs/controller/tag/Tag.js +++ b/docs-web/src/main/webapp/src/app/docs/controller/tag/Tag.js @@ -3,7 +3,7 @@ /** * Tag controller. */ -angular.module('docs').controller('Tag', function($scope, Restangular, $state) { +angular.module('docs').controller('Tag', function($scope, Restangular) { $scope.tag = { name: '', color: '#3a87ad' }; // Retrieve tags diff --git a/docs-web/src/main/webapp/src/index.html b/docs-web/src/main/webapp/src/index.html index b63cd652..f6b70f99 100644 --- a/docs-web/src/main/webapp/src/index.html +++ b/docs-web/src/main/webapp/src/index.html @@ -66,6 +66,7 @@ + diff --git a/docs-web/src/main/webapp/src/partial/docs/document.add.tag.html b/docs-web/src/main/webapp/src/partial/docs/document.add.tag.html new file mode 100644 index 00000000..993c9724 --- /dev/null +++ b/docs-web/src/main/webapp/src/partial/docs/document.add.tag.html @@ -0,0 +1,19 @@ + +
        + + +
        diff --git a/docs-web/src/main/webapp/src/partial/docs/document.html b/docs-web/src/main/webapp/src/partial/docs/document.html index 7580c0d9..aa13682d 100644 --- a/docs-web/src/main/webapp/src/partial/docs/document.html +++ b/docs-web/src/main/webapp/src/partial/docs/document.html @@ -174,12 +174,22 @@ ng-if="navigatedTag"> + + +
        diff --git a/docs-web/src/main/webapp/src/partial/docs/tag.html b/docs-web/src/main/webapp/src/partial/docs/tag.html index addc048f..a2f3e2dc 100644 --- a/docs-web/src/main/webapp/src/partial/docs/tag.html +++ b/docs-web/src/main/webapp/src/partial/docs/tag.html @@ -20,7 +20,7 @@ ng-maxlength="36" required ng-model="tag.name" ui-validate="{ space: '!$value || $value.indexOf(\' \') == -1' }"> {{ 'add' | translate }}

        - {{ 'validation.no_space' | translate }} + {{ 'validation.no_space' | translate }}
        - \ No newline at end of file + + +

        +

        + +
        +
        + +
        + +
        +
        + +
        + +
        + +
        +
        + +
        +
        + +
        +
        +
        + +
        + + + + + + + + + + + + + + +
        {{ 'settings.config.webhook_event' | translate }}{{ 'settings.config.webhook_url' | translate }}{{ 'settings.config.webhook_create_date' | translate }}
        {{ webhook.event }}{{ webhook.url }}{{ webhook.create_date | date: dateFormat }} + +
        +
        \ No newline at end of file From bb9957be24ef008af135ab86623d583a8df06cb7 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Wed, 17 Oct 2018 11:40:42 +0200 Subject: [PATCH 272/288] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index c933f4c1..6bb4483a 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ Features - 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 - [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 From 26d9d826a3e287752df11007bbe9e0514c7df330 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Wed, 17 Oct 2018 16:51:07 +0200 Subject: [PATCH 273/288] fix permission query --- .../src/main/java/com/sismics/docs/core/dao/AclDao.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/AclDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/AclDao.java index 31743a5f..2b32396b 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/AclDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/AclDao.java @@ -128,8 +128,9 @@ public class AclDao { StringBuilder sb = new StringBuilder("select a.ACL_ID_C from T_ACL a "); sb.append(" where a.ACL_TARGETID_C in (:targetIdList) and a.ACL_SOURCEID_C = :sourceId and a.ACL_PERM_C = :perm and a.ACL_DELETEDATE_D is null "); sb.append(" union all "); - sb.append(" select a.ACL_ID_C from T_ACL a, T_DOCUMENT_TAG dt "); - sb.append(" where a.ACL_SOURCEID_C = dt.DOT_IDTAG_C and dt.DOT_IDDOCUMENT_C = :sourceId and dt.DOT_DELETEDATE_D is null "); + sb.append(" select a.ACL_ID_C from T_ACL a, T_DOCUMENT_TAG dt, T_DOCUMENT d "); + sb.append(" where a.ACL_SOURCEID_C = dt.DOT_IDTAG_C and dt.DOT_IDDOCUMENT_C = d.DOC_ID_C and dt.DOT_DELETEDATE_D is null "); + sb.append(" and d.DOC_ID_C = :sourceId and d.DOC_DELETEDATE_D is null "); sb.append(" and a.ACL_TARGETID_C in (:targetIdList) and a.ACL_PERM_C = :perm and a.ACL_DELETEDATE_D is null "); Query q = em.createNativeQuery(sb.toString()); q.setParameter("sourceId", sourceId); From 58af2b00cb5f8bc5d37bc6e1e4835531c3fdb1f4 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Thu, 18 Oct 2018 11:32:42 +0200 Subject: [PATCH 274/288] fix api doc --- .../java/com/sismics/docs/rest/resource/FileResource.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java index 53ffc18c..e972d197 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java @@ -377,7 +377,7 @@ public class FileResource extends BaseResource { /** * Returns files linked to a document or not linked to any document. * - * @api {post} /file/list Get files + * @api {get} /file/list Get files * @apiName GetFileList * @apiGroup File * @apiParam {String} id Document ID @@ -507,7 +507,7 @@ public class FileResource extends BaseResource { /** * Returns a file. * - * @api {delete} /file/:id/data Get a file data + * @api {get} /file/:id/data Get a file data * @apiName GetFile * @apiGroup File * @apiParam {String} id File ID From 0c257b763d5f32fa8009aa65092e48a382829e86 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Thu, 18 Oct 2018 18:57:06 +0200 Subject: [PATCH 275/288] Closes #245: admin group undeletable + admin can see all --- .../com/sismics/docs/core/dao/AclDao.java | 5 +++++ .../com/sismics/docs/core/dao/TagDao.java | 3 ++- .../sismics/docs/core/util/SecurityUtil.java | 12 ++++++++++ .../util/indexing/LuceneIndexingHandler.java | 13 ++++++----- .../docs/rest/resource/DocumentResource.java | 2 +- .../docs/rest/resource/GroupResource.java | 12 ++++++++++ .../docs/rest/resource/UserResource.java | 6 ++--- .../sismics/docs/rest/TestGroupResource.java | 22 +++++++++++++------ .../sismics/docs/rest/TestRouteResource.java | 18 +++++++-------- pom.xml | 2 +- 10 files changed, 67 insertions(+), 28 deletions(-) diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/AclDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/AclDao.java index 2b32396b..328bf1ac 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/AclDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/AclDao.java @@ -7,6 +7,7 @@ import com.sismics.docs.core.constant.PermType; import com.sismics.docs.core.dao.dto.AclDto; import com.sismics.docs.core.model.jpa.Acl; import com.sismics.docs.core.util.AuditLogUtil; +import com.sismics.docs.core.util.SecurityUtil; import com.sismics.util.context.ThreadLocalContext; import javax.persistence.EntityManager; @@ -124,6 +125,10 @@ public class AclDao { * @return True if the document is accessible */ public boolean checkPermission(String sourceId, PermType perm, List targetIdList) { + if (SecurityUtil.skipAclCheck(targetIdList)) { + return true; + } + EntityManager em = ThreadLocalContext.get().getEntityManager(); StringBuilder sb = new StringBuilder("select a.ACL_ID_C from T_ACL a "); sb.append(" where a.ACL_TARGETID_C in (:targetIdList) and a.ACL_SOURCEID_C = :sourceId and a.ACL_PERM_C = :perm and a.ACL_DELETEDATE_D is null "); diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/TagDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/TagDao.java index 52ac570a..7a51071e 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/TagDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/TagDao.java @@ -7,6 +7,7 @@ import com.sismics.docs.core.dao.dto.TagDto; import com.sismics.docs.core.model.jpa.DocumentTag; import com.sismics.docs.core.model.jpa.Tag; import com.sismics.docs.core.util.AuditLogUtil; +import com.sismics.docs.core.util.SecurityUtil; import com.sismics.docs.core.util.jpa.QueryParam; import com.sismics.docs.core.util.jpa.QueryUtil; import com.sismics.docs.core.util.jpa.SortCriteria; @@ -185,7 +186,7 @@ public class TagDao { criteriaList.add("t.TAG_ID_C = :id"); parameterMap.put("id", criteria.getId()); } - if (criteria.getTargetIdList() != null) { + if (criteria.getTargetIdList() != null && !SecurityUtil.skipAclCheck(criteria.getTargetIdList())) { sb.append(" left join T_ACL a on a.ACL_TARGETID_C in (:targetIdList) and a.ACL_SOURCEID_C = t.TAG_ID_C and a.ACL_PERM_C = 'READ' and a.ACL_DELETEDATE_D is null "); criteriaList.add("a.ACL_ID_C is not null"); parameterMap.put("targetIdList", criteria.getTargetIdList()); diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/SecurityUtil.java b/docs-core/src/main/java/com/sismics/docs/core/util/SecurityUtil.java index 8142e930..8ab08151 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/util/SecurityUtil.java +++ b/docs-core/src/main/java/com/sismics/docs/core/util/SecurityUtil.java @@ -6,6 +6,8 @@ import com.sismics.docs.core.dao.UserDao; import com.sismics.docs.core.model.jpa.Group; import com.sismics.docs.core.model.jpa.User; +import java.util.List; + /** * Security utilities. * @@ -37,4 +39,14 @@ public class SecurityUtil { return null; } + + /** + * Return true if the ACL targets provided don't need security checks (administrator users). + * + * @param targetIdList Target ID list + * @return True if skip ACL checks + */ + public static boolean skipAclCheck(List targetIdList) { + return targetIdList.contains("admin") || targetIdList.contains("administrators"); + } } diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/indexing/LuceneIndexingHandler.java b/docs-core/src/main/java/com/sismics/docs/core/util/indexing/LuceneIndexingHandler.java index 3e53c17e..792ee54b 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/util/indexing/LuceneIndexingHandler.java +++ b/docs-core/src/main/java/com/sismics/docs/core/util/indexing/LuceneIndexingHandler.java @@ -12,6 +12,7 @@ import com.sismics.docs.core.model.jpa.Config; import com.sismics.docs.core.model.jpa.Document; import com.sismics.docs.core.model.jpa.File; import com.sismics.docs.core.util.DirectoryUtil; +import com.sismics.docs.core.util.SecurityUtil; import com.sismics.docs.core.util.jpa.PaginatedList; import com.sismics.docs.core.util.jpa.PaginatedLists; import com.sismics.docs.core.util.jpa.QueryParam; @@ -229,11 +230,13 @@ public class LuceneIndexingHandler implements IndexingHandler { // Add search criterias if (criteria.getTargetIdList() != null) { - // Read permission is enough for searching - sb.append(" left join T_ACL a on a.ACL_TARGETID_C in (:targetIdList) and a.ACL_SOURCEID_C = d.DOC_ID_C and a.ACL_PERM_C = 'READ' and a.ACL_DELETEDATE_D is null "); - sb.append(" left join T_DOCUMENT_TAG dta on dta.DOT_IDDOCUMENT_C = d.DOC_ID_C and dta.DOT_DELETEDATE_D is null "); - sb.append(" left join T_ACL a2 on a2.ACL_TARGETID_C in (:targetIdList) and a2.ACL_SOURCEID_C = dta.DOT_IDTAG_C and a2.ACL_PERM_C = 'READ' and a2.ACL_DELETEDATE_D is null "); - criteriaList.add("(a.ACL_ID_C is not null or a2.ACL_ID_C is not null)"); + if (!SecurityUtil.skipAclCheck(criteria.getTargetIdList())) { + // Read permission is enough for searching + sb.append(" left join T_ACL a on a.ACL_TARGETID_C in (:targetIdList) and a.ACL_SOURCEID_C = d.DOC_ID_C and a.ACL_PERM_C = 'READ' and a.ACL_DELETEDATE_D is null "); + sb.append(" left join T_DOCUMENT_TAG dta on dta.DOT_IDDOCUMENT_C = d.DOC_ID_C and dta.DOT_DELETEDATE_D is null "); + sb.append(" left join T_ACL a2 on a2.ACL_TARGETID_C in (:targetIdList) and a2.ACL_SOURCEID_C = dta.DOT_IDTAG_C and a2.ACL_PERM_C = 'READ' and a2.ACL_DELETEDATE_D is null "); + criteriaList.add("(a.ACL_ID_C is not null or a2.ACL_ID_C is not null)"); + } parameterMap.put("targetIdList", criteria.getTargetIdList()); } if (!Strings.isNullOrEmpty(criteria.getSearch()) || !Strings.isNullOrEmpty(criteria.getFullSearch())) { diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/DocumentResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/DocumentResource.java index aa3da7b3..78798adc 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/DocumentResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/DocumentResource.java @@ -377,7 +377,7 @@ public class DocumentResource extends BaseResource { } for (DocumentDto documentDto : paginatedList.getResultList()) { - // Get tags added by the current user on this document + // Get tags accessible by the current user on this document List tagDtoList = tagDao.findByCriteria(new TagCriteria() .setTargetIdList(getTargetIdList(null)) .setDocumentId(documentDto.getId()), new SortCriteria(1, true)); diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/GroupResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/GroupResource.java index 4fcb473e..6e9e7b58 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/GroupResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/GroupResource.java @@ -1,7 +1,9 @@ package com.sismics.docs.rest.resource; import com.google.common.base.Strings; +import com.google.common.collect.Sets; import com.sismics.docs.core.dao.GroupDao; +import com.sismics.docs.core.dao.RoleBaseFunctionDao; import com.sismics.docs.core.dao.UserDao; import com.sismics.docs.core.dao.criteria.GroupCriteria; import com.sismics.docs.core.dao.criteria.UserCriteria; @@ -24,6 +26,7 @@ import javax.ws.rs.*; import javax.ws.rs.core.Response; import java.text.MessageFormat; import java.util.List; +import java.util.Set; /** * Group REST resources. @@ -185,6 +188,15 @@ public class GroupResource extends BaseResource { if (group == null) { throw new NotFoundException(); } + + // Ensure that the admin group is not deleted + if (group.getRoleId() != null) { + RoleBaseFunctionDao roleBaseFunctionDao = new RoleBaseFunctionDao(); + Set baseFunctionSet = roleBaseFunctionDao.findByRoleId(Sets.newHashSet(group.getRoleId())); + if (baseFunctionSet.contains(BaseFunction.ADMIN.name())) { + throw new ClientException("ForbiddenError", "The administrators group cannot be deleted"); + } + } // Delete the group groupDao.delete(group.getId(), principal.getId()); diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java index 785f107b..ed0df960 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java @@ -445,7 +445,7 @@ public class UserResource extends BaseResource { throw new ForbiddenClientException(); } - // Ensure that the admin user is not deleted + // Ensure that the admin or guest users are not deleted if (hasBaseFunction(BaseFunction.ADMIN) || principal.isGuest()) { throw new ClientException("ForbiddenError", "This user cannot be deleted"); } @@ -519,8 +519,8 @@ public class UserResource extends BaseResource { } // Ensure that the admin user is not deleted - RoleBaseFunctionDao userBaseFuction = new RoleBaseFunctionDao(); - Set baseFunctionSet = userBaseFuction.findByRoleId(Sets.newHashSet(user.getRoleId())); + RoleBaseFunctionDao roleBaseFunctionDao = new RoleBaseFunctionDao(); + Set baseFunctionSet = roleBaseFunctionDao.findByRoleId(Sets.newHashSet(user.getRoleId())); if (baseFunctionSet.contains(BaseFunction.ADMIN.name())) { throw new ClientException("ForbiddenError", "The admin user cannot be deleted"); } diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestGroupResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestGroupResource.java index 1d5d37a9..785ea611 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestGroupResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestGroupResource.java @@ -1,17 +1,16 @@ package com.sismics.docs.rest; -import java.util.ArrayList; -import java.util.List; +import com.sismics.util.filter.TokenBasedSecurityFilter; +import org.junit.Assert; +import org.junit.Test; import javax.json.JsonArray; import javax.json.JsonObject; import javax.ws.rs.client.Entity; import javax.ws.rs.core.Form; - -import org.junit.Assert; -import org.junit.Test; - -import com.sismics.util.filter.TokenBasedSecurityFilter; +import javax.ws.rs.core.Response; +import java.util.ArrayList; +import java.util.List; /** @@ -167,6 +166,15 @@ public class TestGroupResource extends BaseJerseyTest { target().path("/group/g1").request() .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) .delete(JsonObject.class); + + // Delete group administrators + Response response = target().path("/group/administrators").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) + .delete(); + Assert.assertEquals(Response.Status.BAD_REQUEST, Response.Status.fromStatusCode(response.getStatus())); + json = response.readEntity(JsonObject.class); + Assert.assertEquals("ForbiddenError", json.getString("type")); + Assert.assertEquals("The administrators group cannot be deleted", json.getString("message")); // Check group1 groups (all computed groups) json = target().path("/user").request() diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestRouteResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestRouteResource.java index a9bf279a..5eac656b 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestRouteResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestRouteResource.java @@ -216,7 +216,7 @@ public class TestRouteResource extends BaseJerseyTest { .param("documentId", document1Id) .param("transition", "APPROVED")), JsonObject.class); Assert.assertFalse(json.containsKey("route_step")); - Assert.assertFalse(json.getBoolean("readable")); + Assert.assertTrue(json.getBoolean("readable")); // Admin can read everything Assert.assertTrue(popEmail().contains("workflow step")); // Get the route on document 1 @@ -239,10 +239,9 @@ public class TestRouteResource extends BaseJerseyTest { Assert.assertEquals("APPROVED", step.getString("transition")); // Get document 1 as admin - Response response = target().path("/document/" + document1Id).request() + target().path("/document/" + document1Id).request() .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) - .get(); - Assert.assertEquals(Response.Status.NOT_FOUND, Response.Status.fromStatusCode(response.getStatus())); + .get(JsonObject.class); // Get document 1 as route1 json = target().path("/document/" + document1Id).request() @@ -265,7 +264,7 @@ public class TestRouteResource extends BaseJerseyTest { .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) .get(JsonObject.class); documents = json.getJsonArray("documents"); - Assert.assertEquals(0, documents.size()); + Assert.assertEquals(1, documents.size()); // Admin can read all documents // Start the default route on document 1 target().path("/route/start").request() @@ -282,7 +281,7 @@ public class TestRouteResource extends BaseJerseyTest { Assert.assertTrue(json.containsKey("route_step")); // Get document 1 as admin - response = target().path("/document/" + document1Id).request() + Response response = target().path("/document/" + document1Id).request() .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) .get(); Assert.assertEquals(Response.Status.OK, Response.Status.fromStatusCode(response.getStatus())); @@ -328,10 +327,9 @@ public class TestRouteResource extends BaseJerseyTest { Assert.assertFalse(json.containsKey("route_step")); // Get document 1 as admin - response = target().path("/document/" + document1Id).request() + target().path("/document/" + document1Id).request() .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) - .get(); - Assert.assertEquals(Response.Status.NOT_FOUND, Response.Status.fromStatusCode(response.getStatus())); + .get(JsonObject.class); // Admin can see all documents // List all documents with route1 json = target().path("/document/list") @@ -348,7 +346,7 @@ public class TestRouteResource extends BaseJerseyTest { .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) .get(JsonObject.class); documents = json.getJsonArray("documents"); - Assert.assertEquals(0, documents.size()); + Assert.assertEquals(1, documents.size()); } /** diff --git a/pom.xml b/pom.xml index 3525f792..daf7068d 100644 --- a/pom.xml +++ b/pom.xml @@ -33,7 +33,7 @@ 0.3m 5.5.0 4.2 - 2.0.8 + 2.0.12 1.54 2.9.2 5.1.0.Final From 867c3207c5bf4953c3232989f1bab0a9e9275795 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Thu, 18 Oct 2018 22:22:40 +0200 Subject: [PATCH 276/288] #246: CURL examples for API authentication --- docs-web/src/main/webapp/header.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/docs-web/src/main/webapp/header.md b/docs-web/src/main/webapp/header.md index 64d86490..fadfe52c 100644 --- a/docs-web/src/main/webapp/header.md +++ b/docs-web/src/main/webapp/header.md @@ -27,20 +27,23 @@ All dates are returned in UNIX timestamp format in milliseconds. ## Authentication #### **Step 1: [POST /user/login](#api-User-PostUserLogin)** -A call to this endpoint will return a cookie header like this: +A call to this endpoint will return a cookie header. Here is a CURL example: ``` -HTTP Response: +curl -i -X POST -d username=admin -d password=admin https://docs.mycompany.com/api/user/login Set-Cookie: auth_token=64085630-2ae6-415c-9a92-4b22c107eaa4 ``` #### **Step 2: Authenticated API calls** -All following API calls must have a cookie header supplying the given token, like this: +All following API calls must have a cookie header supplying the given token. Here is a CURL example: ``` -HTTP Request: -Cookie: auth_token=64085630-2ae6-415c-9a92-4b22c107eaa4 +curl -i -X GET -H "Cookie: auth_token=64085630-2ae6-415c-9a92-4b22c107eaa4" https://docs.mycompany.com/api/document/list +{"total":12,"documents":[...]} ``` #### **Step 3: [POST /user/logout](#api-User-PostUserLogout)** A call to this API with a given `auth_token` cookie will make it unusable for other calls. +``` +curl -i -X POST -H "Cookie: auth_token=64085630-2ae6-415c-9a92-4b22c107eaa4" https://docs.mycompany.com/api/user/logout +``` \ No newline at end of file From 108d5ae8301254c1df9cd11317bb432aced8fce5 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Thu, 18 Oct 2018 23:57:08 +0200 Subject: [PATCH 277/288] Android: target api 28 --- docs-android/app/build.gradle | 26 +++++++++---------- .../gradle/wrapper/gradle-wrapper.properties | 4 +-- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/docs-android/app/build.gradle b/docs-android/app/build.gradle index 89572db6..25cfc4f4 100644 --- a/docs-android/app/build.gradle +++ b/docs-android/app/build.gradle @@ -4,7 +4,7 @@ buildscript { google() } dependencies { - classpath 'com.android.tools.build:gradle:3.0.1' + classpath 'com.android.tools.build:gradle:3.2.1' } } apply plugin: 'com.android.application' @@ -15,11 +15,11 @@ repositories { } android { - compileSdkVersion 26 + compileSdkVersion 28 defaultConfig { minSdkVersion 14 - targetSdkVersion 26 + targetSdkVersion 28 versionCode 1 versionName '1.0' } @@ -30,14 +30,14 @@ android { } dependencies { - compile fileTree(dir: 'libs', include: '*.jar') - compile 'com.android.support:appcompat-v7:26.1.0' - compile 'com.android.support:recyclerview-v7:26.1.0' - compile 'com.android.support:design:26.1.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.7.0' - compile 'com.squareup.okhttp3:okhttp-urlconnection:3.4.0' - compile 'com.jakewharton.picasso:picasso2-okhttp3-downloader:1.1.0' + 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/gradle/wrapper/gradle-wrapper.properties b/docs-android/gradle/wrapper/gradle-wrapper.properties index 0d66fbab..84239c64 100644 --- a/docs-android/gradle/wrapper/gradle-wrapper.properties +++ b/docs-android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Tue Nov 14 23:55:56 CET 2017 +#Thu Oct 18 22:37:49 CEST 2018 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip From d4fe719a2cc45c0cbc8117d92293be1898287d2a Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Fri, 19 Oct 2018 13:34:45 +0200 Subject: [PATCH 278/288] hide toolbar and fab on doc list scroll --- docs-android/app/src/main/AndroidManifest.xml | 4 ++ .../sismics/docs/activity/MainActivity.java | 24 ++++++++-- .../docs/fragment/DocListFragment.java | 22 ++------- .../sismics/docs/ui/ScrollingFABBehavior.java | 47 +++++++++++++++++++ .../src/main/res/layout/doc_list_fragment.xml | 14 ------ .../app/src/main/res/layout/main_activity.xml | 44 +++++++++++++++-- .../app/src/main/res/values/styles.xml | 8 ++++ 7 files changed, 121 insertions(+), 42 deletions(-) create mode 100644 docs-android/app/src/main/java/com/sismics/docs/ui/ScrollingFABBehavior.java 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/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"> + + + + + + + + + + + + + @color/colorAccent + +