From cbf31cbad9950f0b61569f228ef6e3424b644afd Mon Sep 17 00:00:00 2001 From: Giuseppe Careri Date: Wed, 31 May 2017 10:44:46 +0200 Subject: [PATCH 1/1] Built motion from commit 17d0c2b.|2.0.0 --- .../angular-material-assets/icons/avatar-icons.svg | 240 + .../assets/angular-material-assets/img/100-0.jpeg | Bin 0 -> 3054 bytes .../assets/angular-material-assets/img/100-1.jpeg | Bin 0 -> 2439 bytes .../assets/angular-material-assets/img/100-2.jpeg | Bin 0 -> 3004 bytes .../assets/angular-material-assets/img/angular.png | Bin 0 -> 2032 bytes public/assets/angular-material-assets/img/bg9.jpg | Bin 0 -> 176083 bytes .../angular-material-assets/img/bgWhitePaper.jpg | Bin 0 -> 11602 bytes .../angular-material-assets/img/docArrow.png | Bin 0 -> 171 bytes .../assets/angular-material-assets/img/donut.jpg | Bin 0 -> 44703 bytes .../img/icons/addShoppingCart.svg | 1 + .../angular-material-assets/img/icons/android.svg | 1 + .../img/icons/angular-logo.svg | 16 + .../img/icons/bower-logo.svg | 55 + .../angular-material-assets/img/icons/cake.svg | 1 + .../img/icons/codepen-logo.svg | 1 + .../angular-material-assets/img/icons/copy.svg | 1 + .../angular-material-assets/img/icons/copy2.svg | 1 + .../angular-material-assets/img/icons/facebook.svg | 1 + .../angular-material-assets/img/icons/favorite.svg | 4 + .../img/icons/github-icon.svg | 3 + .../angular-material-assets/img/icons/github.svg | 19 + .../angular-material-assets/img/icons/hangout.svg | 1 + .../img/icons/ic_access_time_24px.svg | 27 + .../img/icons/ic_arrow_back_24px.svg | 23 + .../img/icons/ic_build_24px.svg | 4 + .../img/icons/ic_card_giftcard_24px.svg | 4 + .../img/icons/ic_chevron_right_24px.svg | 23 + .../img/icons/ic_close_24px.svg | 1 + .../img/icons/ic_code_24px.svg | 4 + .../img/icons/ic_comment_24px.svg | 24 + .../img/icons/ic_email_24px.svg | 4 + .../img/icons/ic_euro_24px.svg | 10 + .../img/icons/ic_insert_drive_file_24px.svg | 6 + .../img/icons/ic_label_24px.svg | 23 + .../img/icons/ic_launch_24px.svg | 24 + .../img/icons/ic_menu_24px.svg | 23 + .../img/icons/ic_more_vert_24px.svg | 24 + .../img/icons/ic_ondemand_video_24px.svg | 4 + .../img/icons/ic_people_24px.svg | 27 + .../img/icons/ic_person_24px.svg | 4 + .../img/icons/ic_phone_24px.svg | 4 + .../img/icons/ic_photo_24px.svg | 24 + .../img/icons/ic_place_24px.svg | 4 + .../img/icons/ic_play_arrow_24px.svg | 23 + .../img/icons/ic_play_circle_fill_24px.svg | 4 + .../img/icons/ic_refresh_24px.svg | 24 + .../img/icons/ic_school_24px.svg | 4 + .../img/icons/ic_visibility_24px.svg | 24 + .../angular-material-assets/img/icons/launch.svg | 4 + .../img/icons/list_control_down.png | Bin 0 -> 197 bytes .../angular-material-assets/img/icons/mail.svg | 1 + .../angular-material-assets/img/icons/menu.svg | 4 + .../angular-material-assets/img/icons/message.svg | 1 + .../img/icons/more_vert.svg | 4 + .../angular-material-assets/img/icons/npm-logo.svg | 8 + .../img/icons/octicon-repo.svg | 5 + .../angular-material-assets/img/icons/print.svg | 1 + .../img/icons/separator.svg | 9 + .../img/icons/sets/communication-icons.svg | 41 + .../img/icons/sets/core-icons.svg | 33 + .../img/icons/sets/device-icons.svg | 77 + .../img/icons/sets/social-icons.svg | 26 + .../img/icons/share-arrow.svg | 1 + .../img/icons/tabs-arrow.svg | 16 + .../img/icons/toggle-arrow.svg | 4 + .../angular-material-assets/img/icons/twitter.svg | 2 + .../angular-material-assets/img/icons/upload.svg | 1 + .../angular-material-assets/img/list/60.jpeg | Bin 0 -> 1193 bytes public/assets/angular-material-assets/img/logo.svg | 12 + .../assets/angular-material-assets/img/mangues.jpg | Bin 0 -> 300522 bytes .../img/testimonials/logo-bradgreen@2x.fw.png | Bin 0 -> 32092 bytes .../img/testimonials/logo-bradgreen@2x.png | Bin 0 -> 8614 bytes .../img/testimonials/logo-maxlynch@2x.fw.png | Bin 0 -> 31128 bytes .../img/testimonials/logo-maxlynch@2x.png | Bin 0 -> 9762 bytes .../img/testimonials/logo-thomasburleson@2x.png | Bin 0 -> 11357 bytes .../img/testimonials/quote.png | Bin 0 -> 611 bytes .../img/testimonials/testimonial-hampton@2x.png | Bin 0 -> 12205 bytes .../img/testimonials/testimonial-holly@2x.png | Bin 0 -> 9916 bytes .../img/testimonials/testimonial-james@2x.png | Bin 0 -> 9876 bytes .../angular-material-assets/img/washedout.png | Bin 0 -> 181729 bytes public/assets/css/custom.css | 47 + public/assets/css/index.css |51480 +++ public/assets/css/vendor.css |33399 ++ public/assets/icons/fonts/icomoon.eot | Bin 0 -> 195068 bytes public/assets/icons/fonts/icomoon.svg | 1299 + public/assets/icons/fonts/icomoon.ttf | Bin 0 -> 194904 bytes public/assets/icons/fonts/icomoon.woff | Bin 0 -> 194980 bytes public/assets/icons/selection.json |29693 ++ public/assets/images/avatars/profile.jpg | Bin 0 -> 1266 bytes public/assets/images/backgrounds/april.jpg | Bin 0 -> 143474 bytes public/assets/images/backgrounds/august.jpg | Bin 0 -> 127639 bytes public/assets/images/backgrounds/december.jpg | Bin 0 -> 158901 bytes public/assets/images/backgrounds/february.jpg | Bin 0 -> 97542 bytes public/assets/images/backgrounds/header-bg.png | Bin 0 -> 208315 bytes public/assets/images/backgrounds/january.jpg | Bin 0 -> 124162 bytes public/assets/images/backgrounds/july.jpg | Bin 0 -> 93693 bytes public/assets/images/backgrounds/june.jpg | Bin 0 -> 126883 bytes public/assets/images/backgrounds/march.jpg | Bin 0 -> 80437 bytes public/assets/images/backgrounds/may.jpg | Bin 0 -> 114381 bytes public/assets/images/backgrounds/november.jpg | Bin 0 -> 95952 bytes public/assets/images/backgrounds/october.jpg | Bin 0 -> 108177 bytes public/assets/images/backgrounds/september.jpg | Bin 0 -> 149179 bytes public/assets/images/business/agents.jpg | Bin 0 -> 5465 bytes public/assets/images/business/companies.jpg | Bin 0 -> 12571 bytes public/assets/images/business/lists.jpg | Bin 0 -> 4200 bytes public/assets/images/business/queues.jpg | Bin 0 -> 5100 bytes public/assets/images/business/telephones.jpg | Bin 0 -> 4771 bytes public/assets/images/business/users.jpg | Bin 0 -> 4937 bytes public/assets/images/flags/ar.png | Bin 0 -> 472 bytes public/assets/images/flags/da.png | Bin 0 -> 495 bytes public/assets/images/flags/de.png | Bin 0 -> 545 bytes public/assets/images/flags/en_EN.png | Bin 0 -> 599 bytes public/assets/images/flags/es.png | Bin 0 -> 469 bytes public/assets/images/flags/fa.png | Bin 0 -> 512 bytes public/assets/images/flags/fi.png | Bin 0 -> 489 bytes public/assets/images/flags/fr.png | Bin 0 -> 545 bytes public/assets/images/flags/hi.png | Bin 0 -> 503 bytes public/assets/images/flags/it.png | Bin 0 -> 420 bytes public/assets/images/flags/ja.png | Bin 0 -> 420 bytes public/assets/images/flags/ko.png | Bin 0 -> 592 bytes public/assets/images/flags/nl.png | Bin 0 -> 453 bytes public/assets/images/flags/no.png | Bin 0 -> 512 bytes public/assets/images/flags/pt-BR.png | Bin 0 -> 593 bytes public/assets/images/flags/pt-PT.png | Bin 0 -> 554 bytes public/assets/images/flags/ru.png | Bin 0 -> 420 bytes public/assets/images/flags/sv.png | Bin 0 -> 542 bytes public/assets/images/flags/tr.png | Bin 0 -> 492 bytes public/assets/images/flags/us.png | Bin 0 -> 609 bytes public/assets/images/flags/zh-CN.png | Bin 0 -> 472 bytes public/assets/images/flags/zh-TW.png | Bin 0 -> 465 bytes public/assets/images/logos/loading.png | Bin 0 -> 8449 bytes public/assets/images/logos/login.png | Bin 0 -> 13397 bytes public/assets/images/logos/whisker_32x32.png | Bin 0 -> 2456 bytes public/assets/images/music-player/error.png | Bin 0 -> 8593 bytes public/assets/images/music-player/success.png | Bin 0 -> 9100 bytes public/assets/images/music-player/uploading.png | Bin 0 -> 10700 bytes .../assets/images/theme-options/content-only.jpg | Bin 0 -> 1440 bytes .../images/theme-options/content-with-toolbar.jpg | Bin 0 -> 2726 bytes .../assets/images/theme-options/horizontal-nav.jpg | Bin 0 -> 2788 bytes .../vertical-nav-with-full-toolbar-2.jpg | Bin 0 -> 3673 bytes .../vertical-nav-with-full-toolbar.jpg | Bin 0 -> 3344 bytes .../assets/images/theme-options/vertical-nav.jpg | Bin 0 -> 3526 bytes public/assets/js/index.min.js | 1 + public/assets/js/vendor.js |359649 ++++++++++++++++++++ public/assets/plugins/mxgraph/css/common.css | 332 + public/assets/plugins/mxgraph/css/explorer.css | 15 + public/assets/plugins/mxgraph/images/button.gif | Bin 0 -> 137 bytes public/assets/plugins/mxgraph/images/close.gif | Bin 0 -> 70 bytes public/assets/plugins/mxgraph/images/collapsed.gif | Bin 0 -> 877 bytes public/assets/plugins/mxgraph/images/error.gif | Bin 0 -> 907 bytes public/assets/plugins/mxgraph/images/expanded.gif | Bin 0 -> 878 bytes public/assets/plugins/mxgraph/images/maximize.gif | Bin 0 -> 843 bytes public/assets/plugins/mxgraph/images/minimize.gif | Bin 0 -> 64 bytes public/assets/plugins/mxgraph/images/normalize.gif | Bin 0 -> 845 bytes public/assets/plugins/mxgraph/images/point.gif | Bin 0 -> 55 bytes public/assets/plugins/mxgraph/images/resize.gif | Bin 0 -> 74 bytes public/assets/plugins/mxgraph/images/separator.gif | Bin 0 -> 146 bytes public/assets/plugins/mxgraph/images/submenu.gif | Bin 0 -> 56 bytes .../assets/plugins/mxgraph/images/transparent.gif | Bin 0 -> 90 bytes public/assets/plugins/mxgraph/images/warning.gif | Bin 0 -> 276 bytes public/assets/plugins/mxgraph/images/warning.png | Bin 0 -> 425 bytes .../assets/plugins/mxgraph/images/window-title.gif | Bin 0 -> 275 bytes public/assets/plugins/mxgraph/images/window.gif | Bin 0 -> 75 bytes public/assets/plugins/mxgraph/mxClient.js | 1 + public/assets/plugins/square/images/checkmark.gif | Bin 0 -> 1253 bytes public/assets/plugins/square/images/clear.gif | Bin 0 -> 1114 bytes public/assets/plugins/square/images/close.png | Bin 0 -> 118 bytes public/assets/plugins/square/images/collapsed.gif | Bin 0 -> 1113 bytes public/assets/plugins/square/images/connector.png | Bin 0 -> 250 bytes public/assets/plugins/square/images/dropdown.gif | Bin 0 -> 1110 bytes public/assets/plugins/square/images/dropdown.png | Bin 0 -> 206 bytes public/assets/plugins/square/images/edit.gif | Bin 0 -> 66 bytes public/assets/plugins/square/images/expanded.gif | Bin 0 -> 1110 bytes public/assets/plugins/square/images/grid.gif | Bin 0 -> 56 bytes .../assets/plugins/square/images/handle-fixed.png | Bin 0 -> 1293 bytes .../assets/plugins/square/images/handle-main.png | Bin 0 -> 379 bytes .../assets/plugins/square/images/handle-rotate.png | Bin 0 -> 3707 bytes .../plugins/square/images/handle-secondary.png | Bin 0 -> 1270 bytes .../plugins/square/images/handle-terminal.png | Bin 0 -> 1286 bytes public/assets/plugins/square/images/help.png | Bin 0 -> 338 bytes public/assets/plugins/square/images/locked.png | Bin 0 -> 1020 bytes public/assets/plugins/square/images/logo.png | Bin 0 -> 8999 bytes public/assets/plugins/square/images/nocolor.png | Bin 0 -> 948 bytes public/assets/plugins/square/images/refresh.png | Bin 0 -> 1766 bytes public/assets/plugins/square/images/round-drop.png | Bin 0 -> 1216 bytes public/assets/plugins/square/images/search.png | Bin 0 -> 404 bytes public/assets/plugins/square/images/tooltip.png | Bin 0 -> 1006 bytes .../assets/plugins/square/images/transparent.gif | Bin 0 -> 90 bytes .../assets/plugins/square/images/triangle-down.png | Bin 0 -> 1104 bytes .../assets/plugins/square/images/triangle-left.png | Bin 0 -> 1123 bytes .../plugins/square/images/triangle-right.png | Bin 0 -> 1103 bytes .../assets/plugins/square/images/triangle-up.png | Bin 0 -> 1086 bytes public/assets/plugins/square/images/unlocked.png | Bin 0 -> 1024 bytes public/assets/plugins/square/js/Actions.js | 1 + public/assets/plugins/square/js/Dialogs.js | 1 + public/assets/plugins/square/js/Editor.js | 1 + public/assets/plugins/square/js/EditorUi.js | 1 + public/assets/plugins/square/js/Graph.js | 1 + public/assets/plugins/square/js/Init.js | 1 + public/assets/plugins/square/js/Menus.js | 1 + public/assets/plugins/square/js/Sidebar.js | 1 + public/assets/plugins/square/js/Toolbar.js | 1 + .../plugins/square/resources/grapheditor.txt | 492 + .../plugins/square/resources/grapheditor_da.txt | 490 + .../plugins/square/resources/grapheditor_de.txt | 490 + .../plugins/square/resources/grapheditor_es.txt | 490 + .../plugins/square/resources/grapheditor_fa.txt | 490 + .../plugins/square/resources/grapheditor_fi.txt | 490 + .../plugins/square/resources/grapheditor_fr.txt | 490 + .../plugins/square/resources/grapheditor_hi.txt | 490 + .../plugins/square/resources/grapheditor_it.txt | 490 + .../plugins/square/resources/grapheditor_ja.txt | 490 + .../plugins/square/resources/grapheditor_ko.txt | 490 + .../plugins/square/resources/grapheditor_nl.txt | 490 + .../plugins/square/resources/grapheditor_no.txt | 490 + .../plugins/square/resources/grapheditor_pt-PT.txt | 490 + .../plugins/square/resources/grapheditor_ru.txt | 490 + .../plugins/square/resources/grapheditor_sv.txt | 490 + .../plugins/square/resources/grapheditor_tr.txt | 490 + .../plugins/square/resources/grapheditor_zh-CN.txt | 490 + .../plugins/square/resources/grapheditor_zh-TW.txt | 490 + public/assets/plugins/square/resources/help.html | 17 + .../assets/plugins/square/resources/help_de.html | 17 + .../stencils/call_management/answer_128x128.png | Bin 0 -> 5966 bytes .../stencils/call_management/callback_128x128.png | Bin 0 -> 2377 bytes .../stencils/call_management/dial_128x128.png | Bin 0 -> 5782 bytes .../stencils/call_management/ext_dial_128x128.png | Bin 0 -> 5385 bytes .../stencils/call_management/hangup_128x128.png | Bin 0 -> 7477 bytes .../stencils/call_management/queue_128x128.png | Bin 0 -> 6290 bytes .../stencils/call_management/voicemail_128x128.png | Bin 0 -> 7469 bytes .../square/stencils/callflow/gotoc_128x128.png | Bin 0 -> 5581 bytes .../square/stencils/callflow/gotoif_128x128.png | Bin 0 -> 7130 bytes .../stencils/callflow/gotoifmultitime_128x128.png | Bin 0 -> 9851 bytes .../stencils/callflow/gotoiftime_128x128.png | Bin 0 -> 7388 bytes .../square/stencils/callflow/vswitch_128x128.png | Bin 0 -> 5410 bytes .../plugins/square/stencils/entry/end_128x128.png | Bin 0 -> 6551 bytes .../square/stencils/entry/finally_128x128.png | Bin 0 -> 7568 bytes .../square/stencils/entry/start_128x128.png | Bin 0 -> 5631 bytes public/assets/plugins/square/stencils/general.xml | 58 + .../square/stencils/general/agi_128x128.png | Bin 0 -> 4113 bytes .../square/stencils/general/custom_app_128x128.png | Bin 0 -> 5273 bytes .../square/stencils/general/noop_128x128.png | Bin 0 -> 5933 bytes .../square/stencils/general/subproject_128x128.png | Bin 0 -> 5143 bytes .../square/stencils/general/system_128x128.png | Bin 0 -> 5866 bytes .../stencils/integration_server/asr_128x128.png | Bin 0 -> 9355 bytes .../integration_server/database_128x128.png | Bin 0 -> 5900 bytes .../integration_server/ispeechasr_128x128.png | Bin 0 -> 4918 bytes .../integration_server/sendFax_128x128.png | Bin 0 -> 4754 bytes .../integration_server/sendMail_128x128.png | Bin 0 -> 3512 bytes .../integration_server/sendSMS_128x128.png | Bin 0 -> 20008 bytes .../stencils/playback/background_128x128.png | Bin 0 -> 6874 bytes .../square/stencils/playback/getdigits_128x128.png | Bin 0 -> 4864 bytes .../stencils/playback/getsecretdigits_128x128.png | Bin 0 -> 5541 bytes .../stencils/playback/ispeechtts_128x128.png | Bin 0 -> 4830 bytes .../square/stencils/playback/menu_128x128.png | Bin 0 -> 5010 bytes .../square/stencils/playback/playback_128x128.png | Bin 0 -> 4903 bytes .../square/stencils/playback/saydigits_128x128.png | Bin 0 -> 6105 bytes .../square/stencils/playback/saynumber_128x128.png | Bin 0 -> 7097 bytes .../stencils/playback/sayphonetic_128x128.png | Bin 0 -> 6642 bytes .../square/stencils/playback/tts_128x128.png | Bin 0 -> 5600 bytes .../square/stencils/recording/record_128x128.png | Bin 0 -> 5155 bytes .../plugins/square/stencils/stats/goal_128x128.png | Bin 0 -> 5807 bytes .../square/stencils/stats/queuelog_128x128.png | Bin 0 -> 4347 bytes .../square/stencils/variable/math_128x128.png | Bin 0 -> 6286 bytes .../square/stencils/variable/set_128x128.png | Bin 0 -> 5854 bytes public/assets/plugins/square/styles/default.xml | 23 + public/assets/plugins/square/styles/down.gif | Bin 0 -> 1113 bytes .../assets/plugins/square/styles/grapheditor.css | 946 + public/assets/plugins/square/styles/help.css | 4 + public/assets/plugins/square/styles/sprites.png | Bin 0 -> 6286 bytes public/assets/plugins/square/styles/thumb_horz.png | Bin 0 -> 113 bytes .../plugins/square/styles/thumb_vertical.png | Bin 0 -> 122 bytes public/assets/plugins/square/styles/up.gif | Bin 0 -> 1113 bytes public/index.html | 106 + server/api/action/action.attributes.js | 1 + server/api/action/action.controller.js | 1 + server/api/action/action.model.js | 1 + server/api/action/action.rpc.js | 1 + server/api/action/index.js | 1 + .../analyticCustomReport.attributes.js | 1 + .../analyticCustomReport.controller.js | 1 + .../analyticCustomReport.model.js | 1 + .../analyticCustomReport.rpc.js | 1 + server/api/analyticCustomReport/index.js | 1 + .../analyticDefaultReport.attributes.js | 1 + .../analyticDefaultReport.controller.js | 1 + .../analyticDefaultReport.model.js | 1 + .../analyticDefaultReport.rpc.js | 1 + server/api/analyticDefaultReport/index.js | 1 + .../analyticExtractedReport.attributes.js | 1 + .../analyticExtractedReport.controller.js | 1 + .../analyticExtractedReport.model.js | 1 + .../analyticExtractedReport.rpc.js | 1 + server/api/analyticExtractedReport/index.js | 1 + .../analyticFieldReport.attributes.js | 1 + .../analyticFieldReport.controller.js | 1 + .../analyticFieldReport.model.js | 1 + .../analyticFieldReport/analyticFieldReport.rpc.js | 1 + server/api/analyticFieldReport/index.js | 1 + .../analyticMetric/analyticMetric.attributes.js | 1 + .../analyticMetric/analyticMetric.controller.js | 1 + server/api/analyticMetric/analyticMetric.model.js | 1 + server/api/analyticMetric/analyticMetric.rpc.js | 1 + server/api/analyticMetric/index.js | 1 + .../analyticTreeReport.attributes.js | 1 + .../analyticTreeReport.controller.js | 1 + .../analyticTreeReport/analyticTreeReport.model.js | 1 + .../analyticTreeReport/analyticTreeReport.rpc.js | 1 + server/api/analyticTreeReport/index.js | 1 + server/api/authGoogle/authGoogle.controller.js | 1 + server/api/authGoogle/index.js | 1 + server/api/authLocal/authLocal.controller.js | 1 + server/api/authLocal/index.js | 1 + server/api/automation/automation.attributes.js | 1 + server/api/automation/automation.controller.js | 1 + server/api/automation/automation.model.js | 1 + server/api/automation/automation.rpc.js | 1 + server/api/automation/index.js | 1 + server/api/campaign/campaign.attributes.js | 1 + server/api/campaign/campaign.controller.js | 1 + server/api/campaign/campaign.model.js | 1 + server/api/campaign/campaign.rpc.js | 1 + server/api/campaign/index.js | 1 + server/api/cannedAnswer/cannedAnswer.attributes.js | 1 + server/api/cannedAnswer/cannedAnswer.controller.js | 1 + server/api/cannedAnswer/cannedAnswer.model.js | 1 + server/api/cannedAnswer/cannedAnswer.rpc.js | 1 + server/api/cannedAnswer/index.js | 1 + server/api/cdr/cdr.attributes.js | 1 + server/api/cdr/cdr.controller.js | 1 + server/api/cdr/cdr.model.js | 1 + server/api/cdr/cdr.rpc.js | 1 + server/api/cdr/index.js | 1 + .../chatApplication/chatApplication.attributes.js | 1 + .../chatApplication/chatApplication.controller.js | 1 + .../api/chatApplication/chatApplication.model.js | 1 + server/api/chatApplication/chatApplication.rpc.js | 1 + server/api/chatApplication/index.js | 1 + .../chatDisposition/chatDisposition.attributes.js | 1 + .../chatDisposition/chatDisposition.controller.js | 1 + .../api/chatDisposition/chatDisposition.model.js | 1 + server/api/chatDisposition/chatDisposition.rpc.js | 1 + server/api/chatDisposition/index.js | 1 + server/api/chatEnquiry/chatEnquiry.attributes.js | 1 + server/api/chatEnquiry/chatEnquiry.controller.js | 1 + server/api/chatEnquiry/chatEnquiry.model.js | 1 + server/api/chatEnquiry/chatEnquiry.rpc.js | 1 + server/api/chatEnquiry/index.js | 1 + .../chatInteraction/chatInteraction.attributes.js | 1 + .../chatInteraction/chatInteraction.controller.js | 1 + .../api/chatInteraction/chatInteraction.model.js | 1 + server/api/chatInteraction/chatInteraction.rpc.js | 1 + server/api/chatInteraction/index.js | 1 + server/api/chatMessage/chatMessage.attributes.js | 1 + server/api/chatMessage/chatMessage.controller.js | 1 + server/api/chatMessage/chatMessage.model.js | 1 + server/api/chatMessage/chatMessage.rpc.js | 1 + server/api/chatMessage/index.js | 1 + .../chatProactiveAction.attributes.js | 1 + .../chatProactiveAction.controller.js | 1 + .../chatProactiveAction.model.js | 1 + .../chatProactiveAction/chatProactiveAction.rpc.js | 1 + server/api/chatProactiveAction/index.js | 1 + server/api/chatQueue/chatQueue.attributes.js | 1 + server/api/chatQueue/chatQueue.controller.js | 1 + server/api/chatQueue/chatQueue.model.js | 1 + server/api/chatQueue/chatQueue.rpc.js | 1 + server/api/chatQueue/index.js | 1 + server/api/chatReport/chatReport.attributes.js | 1 + server/api/chatReport/chatReport.controller.js | 1 + server/api/chatReport/chatReport.model.js | 1 + server/api/chatReport/chatReport.rpc.js | 1 + server/api/chatReport/index.js | 1 + .../chatReportHistory.attributes.js | 1 + .../chatReportHistory.controller.js | 1 + .../chatReportHistory/chatReportHistory.model.js | 1 + .../api/chatReportHistory/chatReportHistory.rpc.js | 1 + server/api/chatReportHistory/index.js | 1 + .../chatSessionReport.attributes.js | 1 + .../chatSessionReport.controller.js | 1 + .../chatSessionReport/chatSessionReport.model.js | 1 + .../api/chatSessionReport/chatSessionReport.rpc.js | 1 + server/api/chatSessionReport/index.js | 1 + .../chatSessionReportHistory.attributes.js | 1 + .../chatSessionReportHistory.controller.js | 1 + .../chatSessionReportHistory.model.js | 1 + .../chatSessionReportHistory.rpc.js | 1 + server/api/chatSessionReportHistory/index.js | 1 + server/api/chatVisitor/chatVisitor.attributes.js | 1 + server/api/chatVisitor/chatVisitor.controller.js | 1 + server/api/chatVisitor/chatVisitor.model.js | 1 + server/api/chatVisitor/chatVisitor.rpc.js | 1 + server/api/chatVisitor/index.js | 1 + server/api/chatWebsite/chatWebsite.attributes.js | 1 + server/api/chatWebsite/chatWebsite.controller.js | 1 + server/api/chatWebsite/chatWebsite.model.js | 1 + server/api/chatWebsite/chatWebsite.rpc.js | 1 + server/api/chatWebsite/index.js | 1 + .../chatWebsiteField.attributes.js | 1 + .../chatWebsiteField.controller.js | 1 + .../api/chatWebsiteField/chatWebsiteField.model.js | 1 + .../api/chatWebsiteField/chatWebsiteField.rpc.js | 1 + server/api/chatWebsiteField/index.js | 1 + server/api/cmCompany/cmCompany.attributes.js | 1 + server/api/cmCompany/cmCompany.controller.js | 1 + server/api/cmCompany/cmCompany.model.js | 1 + server/api/cmCompany/cmCompany.rpc.js | 1 + server/api/cmCompany/index.js | 1 + server/api/cmContact/cmContact.attributes.js | 1 + server/api/cmContact/cmContact.controller.js | 1 + server/api/cmContact/cmContact.model.js | 1 + server/api/cmContact/cmContact.rpc.js | 1 + server/api/cmContact/index.js | 1 + .../api/cmCustomField/cmCustomField.attributes.js | 1 + .../api/cmCustomField/cmCustomField.controller.js | 1 + server/api/cmCustomField/cmCustomField.model.js | 1 + server/api/cmCustomField/cmCustomField.rpc.js | 1 + server/api/cmCustomField/index.js | 1 + server/api/cmHopper/cmHopper.attributes.js | 1 + server/api/cmHopper/cmHopper.controller.js | 1 + server/api/cmHopper/cmHopper.model.js | 1 + server/api/cmHopper/cmHopper.rpc.js | 1 + server/api/cmHopper/index.js | 1 + .../api/cmHopperBlack/cmHopperBlack.attributes.js | 1 + .../api/cmHopperBlack/cmHopperBlack.controller.js | 1 + server/api/cmHopperBlack/cmHopperBlack.model.js | 1 + server/api/cmHopperBlack/cmHopperBlack.rpc.js | 1 + server/api/cmHopperBlack/index.js | 1 + .../api/cmHopperFinal/cmHopperFinal.attributes.js | 1 + .../api/cmHopperFinal/cmHopperFinal.controller.js | 1 + server/api/cmHopperFinal/cmHopperFinal.model.js | 1 + server/api/cmHopperFinal/cmHopperFinal.rpc.js | 1 + server/api/cmHopperFinal/index.js | 1 + .../cmHopperHistory/cmHopperHistory.attributes.js | 1 + .../cmHopperHistory/cmHopperHistory.controller.js | 1 + .../api/cmHopperHistory/cmHopperHistory.model.js | 1 + server/api/cmHopperHistory/cmHopperHistory.rpc.js | 1 + server/api/cmHopperHistory/index.js | 1 + server/api/cmList/cmList.attributes.js | 1 + server/api/cmList/cmList.controller.js | 1 + server/api/cmList/cmList.model.js | 1 + server/api/cmList/cmList.rpc.js | 1 + server/api/cmList/index.js | 1 + server/api/condition/condition.attributes.js | 1 + server/api/condition/condition.controller.js | 1 + server/api/condition/condition.model.js | 1 + server/api/condition/condition.rpc.js | 1 + server/api/condition/index.js | 1 + server/api/dashboard/dashboard.attributes.js | 1 + server/api/dashboard/dashboard.controller.js | 1 + server/api/dashboard/dashboard.model.js | 1 + server/api/dashboard/dashboard.rpc.js | 1 + server/api/dashboard/index.js | 1 + .../api/dashboardItem/dashboardItem.attributes.js | 1 + .../api/dashboardItem/dashboardItem.controller.js | 1 + server/api/dashboardItem/dashboardItem.model.js | 1 + server/api/dashboardItem/dashboardItem.rpc.js | 1 + server/api/dashboardItem/index.js | 1 + server/api/faxAccount/faxAccount.attributes.js | 1 + server/api/faxAccount/faxAccount.controller.js | 1 + server/api/faxAccount/faxAccount.model.js | 1 + server/api/faxAccount/faxAccount.rpc.js | 1 + server/api/faxAccount/index.js | 1 + .../faxApplication/faxApplication.attributes.js | 1 + .../faxApplication/faxApplication.controller.js | 1 + server/api/faxApplication/faxApplication.model.js | 1 + server/api/faxApplication/faxApplication.rpc.js | 1 + server/api/faxApplication/index.js | 1 + .../api/faxAttachment/faxAttachment.attributes.js | 1 + .../api/faxAttachment/faxAttachment.controller.js | 1 + server/api/faxAttachment/faxAttachment.model.js | 1 + server/api/faxAttachment/faxAttachment.rpc.js | 1 + server/api/faxAttachment/index.js | 1 + .../faxDisposition/faxDisposition.attributes.js | 1 + .../faxDisposition/faxDisposition.controller.js | 1 + server/api/faxDisposition/faxDisposition.model.js | 1 + server/api/faxDisposition/faxDisposition.rpc.js | 1 + server/api/faxDisposition/index.js | 1 + .../faxInteraction/faxInteraction.attributes.js | 1 + .../faxInteraction/faxInteraction.controller.js | 1 + server/api/faxInteraction/faxInteraction.model.js | 1 + server/api/faxInteraction/faxInteraction.rpc.js | 1 + server/api/faxInteraction/index.js | 1 + server/api/faxMessage/faxMessage.attributes.js | 1 + server/api/faxMessage/faxMessage.controller.js | 1 + server/api/faxMessage/faxMessage.model.js | 1 + server/api/faxMessage/faxMessage.rpc.js | 1 + server/api/faxMessage/index.js | 1 + server/api/faxQueue/faxQueue.attributes.js | 1 + server/api/faxQueue/faxQueue.controller.js | 1 + server/api/faxQueue/faxQueue.model.js | 1 + server/api/faxQueue/faxQueue.rpc.js | 1 + server/api/faxQueue/index.js | 1 + server/api/faxReport/faxReport.attributes.js | 1 + server/api/faxReport/faxReport.controller.js | 1 + server/api/faxReport/faxReport.model.js | 1 + server/api/faxReport/faxReport.rpc.js | 1 + server/api/faxReport/index.js | 1 + .../faxReportHistory.attributes.js | 1 + .../faxReportHistory.controller.js | 1 + .../api/faxReportHistory/faxReportHistory.model.js | 1 + .../api/faxReportHistory/faxReportHistory.rpc.js | 1 + server/api/faxReportHistory/index.js | 1 + .../faxSessionReport.attributes.js | 1 + .../faxSessionReport.controller.js | 1 + .../api/faxSessionReport/faxSessionReport.model.js | 1 + .../api/faxSessionReport/faxSessionReport.rpc.js | 1 + server/api/faxSessionReport/index.js | 1 + .../faxSessionReportHistory.attributes.js | 1 + .../faxSessionReportHistory.controller.js | 1 + .../faxSessionReportHistory.model.js | 1 + .../faxSessionReportHistory.rpc.js | 1 + server/api/faxSessionReportHistory/index.js | 1 + server/api/intFreshdeskAccount/index.js | 1 + .../intFreshdeskAccount.attributes.js | 1 + .../intFreshdeskAccount.controller.js | 1 + .../intFreshdeskAccount.model.js | 1 + .../intFreshdeskAccount/intFreshdeskAccount.rpc.js | 1 + server/api/intFreshdeskConfiguration/index.js | 1 + .../intFreshdeskConfiguration.attributes.js | 1 + .../intFreshdeskConfiguration.controller.js | 1 + .../intFreshdeskConfiguration.model.js | 1 + .../intFreshdeskConfiguration.rpc.js | 1 + server/api/intFreshdeskField/index.js | 1 + .../intFreshdeskField.attributes.js | 1 + .../intFreshdeskField.controller.js | 1 + .../intFreshdeskField/intFreshdeskField.model.js | 1 + .../api/intFreshdeskField/intFreshdeskField.rpc.js | 1 + server/api/intSalesforceAccount/index.js | 1 + .../intSalesforceAccount.attributes.js | 1 + .../intSalesforceAccount.controller.js | 1 + .../intSalesforceAccount.model.js | 1 + .../intSalesforceAccount.rpc.js | 1 + server/api/intSalesforceConfiguration/index.js | 1 + .../intSalesforceConfiguration.attributes.js | 1 + .../intSalesforceConfiguration.controller.js | 1 + .../intSalesforceConfiguration.model.js | 1 + .../intSalesforceConfiguration.rpc.js | 1 + server/api/intSalesforceField/index.js | 1 + .../intSalesforceField.attributes.js | 1 + .../intSalesforceField.controller.js | 1 + .../intSalesforceField/intSalesforceField.model.js | 1 + .../intSalesforceField/intSalesforceField.rpc.js | 1 + server/api/intSugarcrmAccount/index.js | 1 + .../intSugarcrmAccount.attributes.js | 1 + .../intSugarcrmAccount.controller.js | 1 + .../intSugarcrmAccount/intSugarcrmAccount.model.js | 1 + .../intSugarcrmAccount/intSugarcrmAccount.rpc.js | 1 + server/api/intSugarcrmConfiguration/index.js | 1 + .../intSugarcrmConfiguration.attributes.js | 1 + .../intSugarcrmConfiguration.controller.js | 1 + .../intSugarcrmConfiguration.model.js | 1 + .../intSugarcrmConfiguration.rpc.js | 1 + server/api/intSugarcrmField/index.js | 1 + .../intSugarcrmField.attributes.js | 1 + .../intSugarcrmField.controller.js | 1 + .../api/intSugarcrmField/intSugarcrmField.model.js | 1 + .../api/intSugarcrmField/intSugarcrmField.rpc.js | 1 + server/api/intZendeskAccount/index.js | 1 + .../intZendeskAccount.attributes.js | 1 + .../intZendeskAccount.controller.js | 1 + .../intZendeskAccount/intZendeskAccount.model.js | 1 + .../api/intZendeskAccount/intZendeskAccount.rpc.js | 1 + server/api/intZendeskConfiguration/index.js | 1 + .../intZendeskConfiguration.attributes.js | 1 + .../intZendeskConfiguration.controller.js | 1 + .../intZendeskConfiguration.model.js | 1 + .../intZendeskConfiguration.rpc.js | 1 + server/api/intZendeskField/index.js | 1 + .../intZendeskField/intZendeskField.attributes.js | 1 + .../intZendeskField/intZendeskField.controller.js | 1 + .../api/intZendeskField/intZendeskField.model.js | 1 + server/api/intZendeskField/intZendeskField.rpc.js | 1 + server/api/integration/index.js | 1 + server/api/integration/integration.attributes.js | 1 + server/api/integration/integration.controller.js | 1 + server/api/integration/integration.model.js | 1 + server/api/integration/integration.rpc.js | 1 + server/api/integrationReport/index.js | 1 + .../integrationReport.attributes.js | 1 + .../integrationReport.controller.js | 1 + .../integrationReport/integrationReport.model.js | 1 + .../api/integrationReport/integrationReport.rpc.js | 1 + server/api/integrationReportHistory/index.js | 1 + .../integrationReportHistory.attributes.js | 1 + .../integrationReportHistory.controller.js | 1 + .../integrationReportHistory.model.js | 1 + .../integrationReportHistory.rpc.js | 1 + server/api/interval/index.js | 1 + server/api/interval/interval.attributes.js | 1 + server/api/interval/interval.controller.js | 1 + server/api/interval/interval.model.js | 1 + server/api/interval/interval.rpc.js | 1 + server/api/jira/index.js | 1 + server/api/jira/jira.controller.js | 1 + server/api/jscriptyInputReport/index.js | 1 + .../jscriptyInputReport.attributes.js | 1 + .../jscriptyInputReport.controller.js | 1 + .../jscriptyInputReport.model.js | 1 + .../jscriptyInputReport/jscriptyInputReport.rpc.js | 1 + server/api/jscriptyProject/index.js | 1 + .../jscriptyProject/jscriptyProject.attributes.js | 1 + .../jscriptyProject/jscriptyProject.controller.js | 1 + .../api/jscriptyProject/jscriptyProject.model.js | 1 + server/api/jscriptyProject/jscriptyProject.rpc.js | 1 + server/api/jscriptyQuestionReport/index.js | 1 + .../jscriptyQuestionReport.attributes.js | 1 + .../jscriptyQuestionReport.controller.js | 1 + .../jscriptyQuestionReport.model.js | 1 + .../jscriptyQuestionReport.rpc.js | 1 + server/api/jscriptySessionReport/index.js | 1 + .../jscriptySessionReport.attributes.js | 1 + .../jscriptySessionReport.controller.js | 1 + .../jscriptySessionReport.model.js | 1 + .../jscriptySessionReport.rpc.js | 1 + server/api/license/index.js | 1 + server/api/license/license.attributes.js | 1 + server/api/license/license.controller.js | 1 + server/api/license/license.model.js | 1 + server/api/license/license.rpc.js | 1 + server/api/mailAccount/index.js | 1 + server/api/mailAccount/mailAccount.attributes.js | 1 + server/api/mailAccount/mailAccount.controller.js | 1 + server/api/mailAccount/mailAccount.model.js | 1 + server/api/mailAccount/mailAccount.rpc.js | 1 + server/api/mailApplication/index.js | 1 + .../mailApplication/mailApplication.attributes.js | 1 + .../mailApplication/mailApplication.controller.js | 1 + .../api/mailApplication/mailApplication.model.js | 1 + server/api/mailApplication/mailApplication.rpc.js | 1 + server/api/mailAttachment/index.js | 1 + .../mailAttachment/mailAttachment.attributes.js | 1 + .../mailAttachment/mailAttachment.controller.js | 1 + server/api/mailAttachment/mailAttachment.model.js | 1 + server/api/mailAttachment/mailAttachment.rpc.js | 1 + server/api/mailDisposition/index.js | 1 + .../mailDisposition/mailDisposition.attributes.js | 1 + .../mailDisposition/mailDisposition.controller.js | 1 + .../api/mailDisposition/mailDisposition.model.js | 1 + server/api/mailDisposition/mailDisposition.rpc.js | 1 + server/api/mailInteraction/index.js | 1 + .../mailInteraction/mailInteraction.attributes.js | 1 + .../mailInteraction/mailInteraction.controller.js | 1 + .../api/mailInteraction/mailInteraction.model.js | 1 + server/api/mailInteraction/mailInteraction.rpc.js | 1 + server/api/mailMessage/index.js | 1 + server/api/mailMessage/mailMessage.attributes.js | 1 + server/api/mailMessage/mailMessage.controller.js | 1 + server/api/mailMessage/mailMessage.model.js | 1 + server/api/mailMessage/mailMessage.rpc.js | 1 + server/api/mailQueue/index.js | 1 + server/api/mailQueue/mailQueue.attributes.js | 1 + server/api/mailQueue/mailQueue.controller.js | 1 + server/api/mailQueue/mailQueue.model.js | 1 + server/api/mailQueue/mailQueue.rpc.js | 1 + server/api/mailReport/index.js | 1 + server/api/mailReport/mailReport.attributes.js | 1 + server/api/mailReport/mailReport.controller.js | 1 + server/api/mailReport/mailReport.model.js | 1 + server/api/mailReport/mailReport.rpc.js | 1 + server/api/mailReportHistory/index.js | 1 + .../mailReportHistory.attributes.js | 1 + .../mailReportHistory.controller.js | 1 + .../mailReportHistory/mailReportHistory.model.js | 1 + .../api/mailReportHistory/mailReportHistory.rpc.js | 1 + server/api/mailServerIn/index.js | 1 + server/api/mailServerIn/mailServerIn.attributes.js | 1 + server/api/mailServerIn/mailServerIn.model.js | 1 + server/api/mailServerIn/mailServerIn.rpc.js | 1 + server/api/mailServerOut/index.js | 1 + .../api/mailServerOut/mailServerOut.attributes.js | 1 + .../api/mailServerOut/mailServerOut.controller.js | 1 + server/api/mailServerOut/mailServerOut.model.js | 1 + server/api/mailServerOut/mailServerOut.rpc.js | 1 + server/api/mailSessionReport/index.js | 1 + .../mailSessionReport.attributes.js | 1 + .../mailSessionReport.controller.js | 1 + .../mailSessionReport/mailSessionReport.model.js | 1 + .../api/mailSessionReport/mailSessionReport.rpc.js | 1 + server/api/mailSessionReportHistory/index.js | 1 + .../mailSessionReportHistory.attributes.js | 1 + .../mailSessionReportHistory.controller.js | 1 + .../mailSessionReportHistory.model.js | 1 + .../mailSessionReportHistory.rpc.js | 1 + server/api/memberReport/index.js | 1 + server/api/memberReport/memberReport.attributes.js | 1 + server/api/memberReport/memberReport.controller.js | 1 + server/api/memberReport/memberReport.model.js | 1 + server/api/memberReport/memberReport.rpc.js | 1 + server/api/memberReportHistory/index.js | 1 + .../memberReportHistory.attributes.js | 1 + .../memberReportHistory.controller.js | 1 + .../memberReportHistory.model.js | 1 + .../memberReportHistory/memberReportHistory.rpc.js | 1 + server/api/module/index.js | 1 + server/api/module/module.attributes.js | 1 + server/api/module/module.model.js | 1 + server/api/module/module.rpc.js | 1 + server/api/network/index.js | 1 + server/api/network/network.attributes.js | 1 + server/api/network/network.controller.js | 1 + server/api/network/network.model.js | 1 + server/api/network/network.rpc.js | 1 + server/api/openchannelAccount/index.js | 1 + .../openchannelAccount.attributes.js | 1 + .../openchannelAccount.controller.js | 1 + .../openchannelAccount/openchannelAccount.model.js | 1 + .../openchannelAccount/openchannelAccount.rpc.js | 1 + server/api/openchannelApplication/index.js | 1 + .../openchannelApplication.attributes.js | 1 + .../openchannelApplication.controller.js | 1 + .../openchannelApplication.model.js | 1 + .../openchannelApplication.rpc.js | 1 + server/api/openchannelDisposition/index.js | 1 + .../openchannelDisposition.attributes.js | 1 + .../openchannelDisposition.controller.js | 1 + .../openchannelDisposition.model.js | 1 + .../openchannelDisposition.rpc.js | 1 + server/api/openchannelInteraction/index.js | 1 + .../openchannelInteraction.attributes.js | 1 + .../openchannelInteraction.controller.js | 1 + .../openchannelInteraction.model.js | 1 + .../openchannelInteraction.rpc.js | 1 + server/api/openchannelMessage/index.js | 1 + .../openchannelMessage.attributes.js | 1 + .../openchannelMessage.controller.js | 1 + .../openchannelMessage/openchannelMessage.model.js | 1 + .../openchannelMessage/openchannelMessage.rpc.js | 1 + server/api/openchannelQueue/index.js | 1 + .../openchannelQueue.attributes.js | 1 + .../openchannelQueue.controller.js | 1 + .../api/openchannelQueue/openchannelQueue.model.js | 1 + .../api/openchannelQueue/openchannelQueue.rpc.js | 1 + server/api/openchannelReport/index.js | 1 + .../openchannelReport.attributes.js | 1 + .../openchannelReport.controller.js | 1 + .../openchannelReport/openchannelReport.model.js | 1 + .../api/openchannelReport/openchannelReport.rpc.js | 1 + server/api/openchannelReportHistory/index.js | 1 + .../openchannelReportHistory.attributes.js | 1 + .../openchannelReportHistory.controller.js | 1 + .../openchannelReportHistory.model.js | 1 + .../openchannelReportHistory.rpc.js | 1 + server/api/openchannelSessionReport/index.js | 1 + .../openchannelSessionReport.attributes.js | 1 + .../openchannelSessionReport.controller.js | 1 + .../openchannelSessionReport.model.js | 1 + .../openchannelSessionReport.rpc.js | 1 + .../api/openchannelSessionReportHistory/index.js | 1 + .../openchannelSessionReportHistory.attributes.js | 1 + .../openchannelSessionReportHistory.controller.js | 1 + .../openchannelSessionReportHistory.model.js | 1 + .../openchannelSessionReportHistory.rpc.js | 1 + server/api/pause/index.js | 1 + server/api/pause/pause.attributes.js | 1 + server/api/pause/pause.controller.js | 1 + server/api/pause/pause.model.js | 1 + server/api/pause/pause.rpc.js | 1 + server/api/pm2/index.js | 1 + server/api/pm2/pm2.controller.js | 1 + server/api/rpc/index.js | 1 + server/api/rpc/rpc.controller.js | 1 + server/api/schedule/index.js | 1 + server/api/schedule/schedule.attributes.js | 1 + server/api/schedule/schedule.controller.js | 1 + server/api/schedule/schedule.model.js | 1 + server/api/schedule/schedule.rpc.js | 1 + server/api/setting/index.js | 1 + server/api/setting/setting.attributes.js | 1 + server/api/setting/setting.controller.js | 1 + server/api/setting/setting.model.js | 1 + server/api/setting/setting.rpc.js | 1 + server/api/smsAccount/index.js | 1 + server/api/smsAccount/smsAccount.attributes.js | 1 + server/api/smsAccount/smsAccount.controller.js | 1 + server/api/smsAccount/smsAccount.model.js | 1 + server/api/smsAccount/smsAccount.rpc.js | 1 + server/api/smsApplication/index.js | 1 + .../smsApplication/smsApplication.attributes.js | 1 + .../smsApplication/smsApplication.controller.js | 1 + server/api/smsApplication/smsApplication.model.js | 1 + server/api/smsApplication/smsApplication.rpc.js | 1 + server/api/smsDisposition/index.js | 1 + .../smsDisposition/smsDisposition.attributes.js | 1 + .../smsDisposition/smsDisposition.controller.js | 1 + server/api/smsDisposition/smsDisposition.model.js | 1 + server/api/smsDisposition/smsDisposition.rpc.js | 1 + server/api/smsInteraction/index.js | 1 + .../smsInteraction/smsInteraction.attributes.js | 1 + .../smsInteraction/smsInteraction.controller.js | 1 + server/api/smsInteraction/smsInteraction.model.js | 1 + server/api/smsInteraction/smsInteraction.rpc.js | 1 + server/api/smsMessage/index.js | 1 + server/api/smsMessage/smsMessage.attributes.js | 1 + server/api/smsMessage/smsMessage.controller.js | 1 + server/api/smsMessage/smsMessage.model.js | 1 + server/api/smsMessage/smsMessage.rpc.js | 1 + server/api/smsQueue/index.js | 1 + server/api/smsQueue/smsQueue.attributes.js | 1 + server/api/smsQueue/smsQueue.controller.js | 1 + server/api/smsQueue/smsQueue.model.js | 1 + server/api/smsQueue/smsQueue.rpc.js | 1 + server/api/smsReport/index.js | 1 + server/api/smsReport/smsReport.attributes.js | 1 + server/api/smsReport/smsReport.controller.js | 1 + server/api/smsReport/smsReport.model.js | 1 + server/api/smsReport/smsReport.rpc.js | 1 + server/api/smsReportHistory/index.js | 1 + .../smsReportHistory.attributes.js | 1 + .../smsReportHistory.controller.js | 1 + .../api/smsReportHistory/smsReportHistory.model.js | 1 + .../api/smsReportHistory/smsReportHistory.rpc.js | 1 + server/api/smsSessionReport/index.js | 1 + .../smsSessionReport.attributes.js | 1 + .../smsSessionReport.controller.js | 1 + .../api/smsSessionReport/smsSessionReport.model.js | 1 + .../api/smsSessionReport/smsSessionReport.rpc.js | 1 + server/api/smsSessionReportHistory/index.js | 1 + .../smsSessionReportHistory.attributes.js | 1 + .../smsSessionReportHistory.controller.js | 1 + .../smsSessionReportHistory.model.js | 1 + .../smsSessionReportHistory.rpc.js | 1 + server/api/sound/index.js | 1 + server/api/sound/sound.attributes.js | 1 + server/api/sound/sound.controller.js | 1 + server/api/sound/sound.model.js | 1 + server/api/sound/sound.rpc.js | 1 + server/api/squareOdbc/index.js | 1 + server/api/squareOdbc/squareOdbc.attributes.js | 1 + server/api/squareOdbc/squareOdbc.controller.js | 1 + server/api/squareOdbc/squareOdbc.model.js | 1 + server/api/squareOdbc/squareOdbc.rpc.js | 1 + server/api/squareProject/index.js | 1 + .../api/squareProject/squareProject.attributes.js | 1 + .../api/squareProject/squareProject.controller.js | 1 + server/api/squareProject/squareProject.model.js | 1 + server/api/squareProject/squareProject.rpc.js | 1 + server/api/squareRecording/index.js | 1 + .../squareRecording/squareRecording.attributes.js | 1 + .../squareRecording/squareRecording.controller.js | 1 + .../api/squareRecording/squareRecording.model.js | 1 + server/api/squareRecording/squareRecording.rpc.js | 1 + server/api/squareReport/index.js | 1 + server/api/squareReport/squareReport.attributes.js | 1 + server/api/squareReport/squareReport.controller.js | 1 + server/api/squareReport/squareReport.model.js | 1 + server/api/squareReport/squareReport.rpc.js | 1 + server/api/squareReportDetail/index.js | 1 + .../squareReportDetail.attributes.js | 1 + .../squareReportDetail.controller.js | 1 + .../squareReportDetail/squareReportDetail.model.js | 1 + .../squareReportDetail/squareReportDetail.rpc.js | 1 + server/api/squareReportDetailHistory/index.js | 1 + .../squareReportDetailHistory.attributes.js | 1 + .../squareReportDetailHistory.controller.js | 1 + .../squareReportDetailHistory.model.js | 1 + .../squareReportDetailHistory.rpc.js | 1 + server/api/squareReportHistory/index.js | 1 + .../squareReportHistory.attributes.js | 1 + .../squareReportHistory.controller.js | 1 + .../squareReportHistory.model.js | 1 + .../squareReportHistory/squareReportHistory.rpc.js | 1 + server/api/system/index.js | 1 + server/api/system/system.controller.js | 1 + server/api/tag/index.js | 1 + server/api/tag/tag.attributes.js | 1 + server/api/tag/tag.controller.js | 1 + server/api/tag/tag.model.js | 1 + server/api/tag/tag.rpc.js | 1 + server/api/team/index.js | 1 + server/api/team/team.attributes.js | 1 + server/api/team/team.controller.js | 1 + server/api/team/team.model.js | 1 + server/api/team/team.rpc.js | 1 + server/api/template/index.js | 1 + server/api/template/template.attributes.js | 1 + server/api/template/template.controller.js | 1 + server/api/template/template.model.js | 1 + server/api/template/template.rpc.js | 1 + server/api/tigerDialReport/index.js | 1 + .../tigerDialReport/tigerDialReport.attributes.js | 1 + .../tigerDialReport/tigerDialReport.controller.js | 1 + .../api/tigerDialReport/tigerDialReport.model.js | 1 + server/api/tigerDialReport/tigerDialReport.rpc.js | 1 + server/api/trigger/index.js | 1 + server/api/trigger/trigger.attributes.js | 1 + server/api/trigger/trigger.controller.js | 1 + server/api/trigger/trigger.model.js | 1 + server/api/trigger/trigger.rpc.js | 1 + server/api/trunk/index.js | 1 + server/api/trunk/trunk.attributes.js | 1 + server/api/trunk/trunk.controller.js | 1 + server/api/trunk/trunk.model.js | 1 + server/api/trunk/trunk.rpc.js | 1 + server/api/user/index.js | 1 + server/api/user/user.attributes.js | 1 + server/api/user/user.controller.js | 1 + server/api/user/user.events.js | 1 + server/api/user/user.model.js | 1 + server/api/user/user.rpc.js | 1 + server/api/user/user.socket.js | 1 + server/api/userChatQueue/index.js | 1 + .../api/userChatQueue/userChatQueue.attributes.js | 1 + server/api/userChatQueue/userChatQueue.model.js | 1 + server/api/userChatQueue/userChatQueue.rpc.js | 1 + server/api/userFaxQueue/index.js | 1 + server/api/userFaxQueue/userFaxQueue.attributes.js | 1 + server/api/userFaxQueue/userFaxQueue.model.js | 1 + server/api/userFaxQueue/userFaxQueue.rpc.js | 1 + server/api/userList/index.js | 1 + server/api/userList/userList.attributes.js | 1 + server/api/userList/userList.model.js | 1 + server/api/userList/userList.rpc.js | 1 + server/api/userMailQueue/index.js | 1 + .../api/userMailQueue/userMailQueue.attributes.js | 1 + server/api/userMailQueue/userMailQueue.model.js | 1 + server/api/userMailQueue/userMailQueue.rpc.js | 1 + server/api/userModule/index.js | 1 + server/api/userModule/userModule.attributes.js | 1 + server/api/userModule/userModule.model.js | 1 + server/api/userModule/userModule.rpc.js | 1 + server/api/userOpenchannelQueue/index.js | 1 + .../userOpenchannelQueue.attributes.js | 1 + .../userOpenchannelQueue.model.js | 1 + .../userOpenchannelQueue.rpc.js | 1 + server/api/userSmsQueue/index.js | 1 + server/api/userSmsQueue/userSmsQueue.attributes.js | 1 + server/api/userSmsQueue/userSmsQueue.model.js | 1 + server/api/userSmsQueue/userSmsQueue.rpc.js | 1 + server/api/userVoiceQueue/index.js | 1 + .../userVoiceQueue/userVoiceQueue.attributes.js | 1 + server/api/userVoiceQueue/userVoiceQueue.model.js | 1 + server/api/userVoiceQueue/userVoiceQueue.rpc.js | 1 + server/api/userVoiceQueueRt/index.js | 1 + .../userVoiceQueueRt.attributes.js | 1 + .../userVoiceQueueRt/userVoiceQueueRt.events.js | 1 + .../api/userVoiceQueueRt/userVoiceQueueRt.model.js | 1 + .../api/userVoiceQueueRt/userVoiceQueueRt.rpc.js | 1 + .../userVoiceQueueRt/userVoiceQueueRt.socket.js | 1 + server/api/variable/index.js | 1 + server/api/variable/variable.attributes.js | 1 + server/api/variable/variable.controller.js | 1 + server/api/variable/variable.model.js | 1 + server/api/variable/variable.rpc.js | 1 + server/api/version/index.js | 1 + server/api/version/version.controller.js | 1 + server/api/voiceAgentReport/index.js | 1 + .../voiceAgentReport.attributes.js | 1 + .../voiceAgentReport.controller.js | 1 + .../api/voiceAgentReport/voiceAgentReport.model.js | 1 + .../api/voiceAgentReport/voiceAgentReport.rpc.js | 1 + server/api/voiceAgentReportHistory/index.js | 1 + .../voiceAgentReportHistory.attributes.js | 1 + .../voiceAgentReportHistory.controller.js | 1 + .../voiceAgentReportHistory.model.js | 1 + .../voiceAgentReportHistory.rpc.js | 1 + server/api/voiceCallReport/index.js | 1 + .../voiceCallReport/voiceCallReport.attributes.js | 1 + .../voiceCallReport/voiceCallReport.controller.js | 1 + .../api/voiceCallReport/voiceCallReport.model.js | 1 + server/api/voiceCallReport/voiceCallReport.rpc.js | 1 + server/api/voiceCallReportHistory/index.js | 1 + .../voiceCallReportHistory.attributes.js | 1 + .../voiceCallReportHistory.controller.js | 1 + .../voiceCallReportHistory.model.js | 1 + .../voiceCallReportHistory.rpc.js | 1 + server/api/voiceChanSpy/index.js | 1 + server/api/voiceChanSpy/voiceChanSpy.attributes.js | 1 + server/api/voiceChanSpy/voiceChanSpy.controller.js | 1 + server/api/voiceChanSpy/voiceChanSpy.model.js | 1 + server/api/voiceChanSpy/voiceChanSpy.rpc.js | 1 + server/api/voiceContext/index.js | 1 + server/api/voiceContext/voiceContext.attributes.js | 1 + server/api/voiceContext/voiceContext.controller.js | 1 + server/api/voiceContext/voiceContext.model.js | 1 + server/api/voiceContext/voiceContext.rpc.js | 1 + server/api/voiceDialReport/index.js | 1 + .../voiceDialReport/voiceDialReport.attributes.js | 1 + .../voiceDialReport/voiceDialReport.controller.js | 1 + .../api/voiceDialReport/voiceDialReport.model.js | 1 + server/api/voiceDialReport/voiceDialReport.rpc.js | 1 + server/api/voiceDialReportHistory/index.js | 1 + .../voiceDialReportHistory.attributes.js | 1 + .../voiceDialReportHistory.controller.js | 1 + .../voiceDialReportHistory.model.js | 1 + .../voiceDialReportHistory.rpc.js | 1 + server/api/voiceDisposition/index.js | 1 + .../voiceDisposition.attributes.js | 1 + .../voiceDisposition.controller.js | 1 + .../api/voiceDisposition/voiceDisposition.model.js | 1 + .../api/voiceDisposition/voiceDisposition.rpc.js | 1 + server/api/voiceExtension/index.js | 1 + .../voiceExtension/voiceExtension.attributes.js | 1 + .../voiceExtension/voiceExtension.controller.js | 1 + server/api/voiceExtension/voiceExtension.model.js | 1 + server/api/voiceExtension/voiceExtension.rpc.js | 1 + server/api/voiceMail/index.js | 1 + server/api/voiceMail/voiceMail.attributes.js | 1 + server/api/voiceMail/voiceMail.controller.js | 1 + server/api/voiceMail/voiceMail.model.js | 1 + server/api/voiceMail/voiceMail.rpc.js | 1 + server/api/voiceMailMessage/index.js | 1 + .../voiceMailMessage.attributes.js | 1 + .../voiceMailMessage.controller.js | 1 + .../api/voiceMailMessage/voiceMailMessage.model.js | 1 + .../api/voiceMailMessage/voiceMailMessage.rpc.js | 1 + server/api/voiceMusicOnHold/index.js | 1 + .../voiceMusicOnHold.attributes.js | 1 + .../voiceMusicOnHold.controller.js | 1 + .../api/voiceMusicOnHold/voiceMusicOnHold.model.js | 1 + .../api/voiceMusicOnHold/voiceMusicOnHold.rpc.js | 1 + server/api/voiceQueue/index.js | 1 + server/api/voiceQueue/voiceQueue.attributes.js | 1 + server/api/voiceQueue/voiceQueue.controller.js | 1 + server/api/voiceQueue/voiceQueue.model.js | 1 + server/api/voiceQueue/voiceQueue.rpc.js | 1 + server/api/voiceQueueReport/index.js | 1 + .../voiceQueueReport.attributes.js | 1 + .../voiceQueueReport.controller.js | 1 + .../api/voiceQueueReport/voiceQueueReport.model.js | 1 + .../api/voiceQueueReport/voiceQueueReport.rpc.js | 1 + server/api/voiceQueueReportHistory/index.js | 1 + .../voiceQueueReportHistory.attributes.js | 1 + .../voiceQueueReportHistory.controller.js | 1 + .../voiceQueueReportHistory.model.js | 1 + .../voiceQueueReportHistory.rpc.js | 1 + server/api/voiceQueuesLog/index.js | 1 + .../voiceQueuesLog/voiceQueuesLog.attributes.js | 1 + .../voiceQueuesLog/voiceQueuesLog.controller.js | 1 + server/api/voiceQueuesLog/voiceQueuesLog.model.js | 1 + server/api/voiceQueuesLog/voiceQueuesLog.rpc.js | 1 + server/api/voiceRecording/index.js | 1 + .../voiceRecording/voiceRecording.attributes.js | 1 + .../voiceRecording/voiceRecording.controller.js | 1 + server/api/voiceRecording/voiceRecording.model.js | 1 + server/api/voiceRecording/voiceRecording.rpc.js | 1 + server/api/voiceTransferReport/index.js | 1 + .../voiceTransferReport.attributes.js | 1 + .../voiceTransferReport.controller.js | 1 + .../voiceTransferReport.model.js | 1 + .../voiceTransferReport/voiceTransferReport.rpc.js | 1 + server/api/voiceTransferReportHistory/index.js | 1 + .../voiceTransferReportHistory.attributes.js | 1 + .../voiceTransferReportHistory.controller.js | 1 + .../voiceTransferReportHistory.model.js | 1 + .../voiceTransferReportHistory.rpc.js | 1 + server/app.js | 1 + server/components/auth/service.js | 1 + server/components/dashboards/run.js | 1 + server/components/export/csv.js | 1 + server/components/export/pdf.js | 1 + server/components/export/xls.js | 1 + server/components/extensions/rewrite.js | 1 + server/components/integrations/configuration.js | 1 + server/components/license/service.js | 1 + server/components/parsers/qs.js | 1 + server/config/environment/development.js | 1 + server/config/environment/index.js | 1 + server/config/environment/shared.js | 1 + server/config/express.js | 1 + server/config/license/hardware.js | 1 + server/config/license/index.js | 1 + server/config/local.env.js | 1 + server/config/logger.js | 1 + server/config/pm2/config.js | 1 + server/config/pm2/index.js | 1 + server/config/seed.js | 1 + server/config/seedContact.js | 1 + server/config/seedUsers.js | 1 + server/config/socketio.js | 1 + server/config/utils.js | 1 + server/errors/index.js | 1 + server/mysqldb/index.js | 1 + server/routes.js | 1 + server/rpc/index.js | 1 + server/services/agi.backup/index.js | 1 + server/services/agi.backup/lib/channel.js | 1 + server/services/agi.backup/lib/connection.js | 1 + server/services/agi.backup/lib/reply.js | 1 + server/services/agi.backup/lib/rpc/index.js | 1 + server/services/agi.backup/lib/scripts/index.js | 1 + server/services/agi.backup/lib/server.js | 1 + server/services/agi.backup/lib/square.js | 1 + server/services/agi.backup/lib/vertices.js | 1 + server/services/agi/channel.js | 1 + server/services/agi/error.js | 1 + server/services/agi/graph/index.js | 1 + server/services/agi/index.js | 1 + server/services/agi/reply.js | 1 + server/services/agi/rpc/index.js | 1 + server/services/agi/scripts/index.js | 1 + server/services/agi/server.js | 1 + server/services/agi/vertices.js | 1 + server/services/ami/acw.old.js | 1 + server/services/ami/acw/index.js | 1 + server/services/ami/ami.js | 1 + server/services/ami/client.js | 1 + server/services/ami/dialer/action.js | 1 + server/services/ami/dialer/agentComplete.js | 1 + server/services/ami/dialer/final.js | 1 + server/services/ami/dialer/hangup.js | 1 + server/services/ami/dialer/history.js | 1 + server/services/ami/dialer/index.js | 1 + server/services/ami/dialer/queueCallerAbandon.js | 1 + server/services/ami/index.js | 1 + server/services/ami/model/agent.js | 1 + server/services/ami/model/campaign.js | 1 + server/services/ami/model/channel.js | 1 + server/services/ami/model/freshdeskAccount.js | 1 + server/services/ami/model/queueReport.js | 1 + server/services/ami/model/salesforceAccount.js | 1 + server/services/ami/model/sugarcrmAccount.js | 1 + server/services/ami/model/telephone.js | 1 + server/services/ami/model/template.js | 1 + server/services/ami/model/trunk.js | 1 + server/services/ami/model/voiceQueue.js | 1 + server/services/ami/model/voiceTrigger.js | 1 + server/services/ami/model/zendeskAccount.js | 1 + server/services/ami/properties.js | 1 + server/services/ami/realtime/index.js | 1 + server/services/ami/report.old.js | 1 + server/services/ami/report/index.js | 1 + server/services/ami/report/userReport.js | 1 + server/services/ami/report/voiceAgentReport.js | 1 + server/services/ami/report/voiceCallReport.js | 1 + server/services/ami/report/voiceDialReport.js | 1 + server/services/ami/report/voiceQueueReport.js | 1 + server/services/ami/rpc/campaign.js | 1 + server/services/ami/rpc/cmHopper.js | 1 + server/services/ami/rpc/cmHopperBlack.js | 1 + server/services/ami/rpc/cmHopperFinal.js | 1 + server/services/ami/rpc/cmHopperHistory.js | 1 + server/services/ami/rpc/integrations/freshdesk.js | 1 + server/services/ami/rpc/integrations/salesforce.js | 1 + server/services/ami/rpc/integrations/sugarcrm.js | 1 + server/services/ami/rpc/integrations/zendesk.js | 1 + server/services/ami/rpc/memberReport.js | 1 + server/services/ami/rpc/template.js | 1 + server/services/ami/rpc/trigger.js | 1 + server/services/ami/rpc/trunk.js | 1 + server/services/ami/rpc/user.js | 1 + server/services/ami/rpc/voiceAgentReport.js | 1 + server/services/ami/rpc/voiceCallReport.js | 1 + server/services/ami/rpc/voiceDialReport.js | 1 + server/services/ami/rpc/voiceQueue.js | 1 + server/services/ami/rpc/voiceQueueReport.js | 1 + server/services/ami/rpc/voiceQueueRt.js | 1 + server/services/ami/rpc/voiceRecording.js | 1 + server/services/ami/rpc/voiceTransferReport.js | 1 + server/services/ami/server/index.js | 1 + server/services/ami/trigger/index.js | 1 + .../services/ami/trigger/integration/freshdesk.js | 1 + server/services/ami/trigger/integration/index.js | 1 + .../services/ami/trigger/integration/salesforce.js | 1 + .../services/ami/trigger/integration/sugarcrm.js | 1 + server/services/ami/trigger/integration/zendesk.js | 1 + server/services/ami/trigger/urlforward/index.js | 1 + server/services/email/index.js | 1 + server/services/email/lib/email.js | 1 + server/services/email/lib/imap.js | 1 + server/services/email/lib/smtp.js | 1 + server/services/express/index.js | 1 + 1162 files changed, 488261 insertions(+), 0 deletions(-) create mode 100644 public/assets/angular-material-assets/icons/avatar-icons.svg create mode 100644 public/assets/angular-material-assets/img/100-0.jpeg create mode 100644 public/assets/angular-material-assets/img/100-1.jpeg create mode 100644 public/assets/angular-material-assets/img/100-2.jpeg create mode 100644 public/assets/angular-material-assets/img/angular.png create mode 100644 public/assets/angular-material-assets/img/bg9.jpg create mode 100644 public/assets/angular-material-assets/img/bgWhitePaper.jpg create mode 100644 public/assets/angular-material-assets/img/docArrow.png create mode 100644 public/assets/angular-material-assets/img/donut.jpg create mode 100644 public/assets/angular-material-assets/img/icons/addShoppingCart.svg create mode 100644 public/assets/angular-material-assets/img/icons/android.svg create mode 100644 public/assets/angular-material-assets/img/icons/angular-logo.svg create mode 100644 public/assets/angular-material-assets/img/icons/bower-logo.svg create mode 100644 public/assets/angular-material-assets/img/icons/cake.svg create mode 100644 public/assets/angular-material-assets/img/icons/codepen-logo.svg create mode 100644 public/assets/angular-material-assets/img/icons/copy.svg create mode 100644 public/assets/angular-material-assets/img/icons/copy2.svg create mode 100644 public/assets/angular-material-assets/img/icons/facebook.svg create mode 100644 public/assets/angular-material-assets/img/icons/favorite.svg create mode 100644 public/assets/angular-material-assets/img/icons/github-icon.svg create mode 100644 public/assets/angular-material-assets/img/icons/github.svg create mode 100644 public/assets/angular-material-assets/img/icons/hangout.svg create mode 100644 public/assets/angular-material-assets/img/icons/ic_access_time_24px.svg create mode 100644 public/assets/angular-material-assets/img/icons/ic_arrow_back_24px.svg create mode 100644 public/assets/angular-material-assets/img/icons/ic_build_24px.svg create mode 100644 public/assets/angular-material-assets/img/icons/ic_card_giftcard_24px.svg create mode 100644 public/assets/angular-material-assets/img/icons/ic_chevron_right_24px.svg create mode 100644 public/assets/angular-material-assets/img/icons/ic_close_24px.svg create mode 100644 public/assets/angular-material-assets/img/icons/ic_code_24px.svg create mode 100644 public/assets/angular-material-assets/img/icons/ic_comment_24px.svg create mode 100644 public/assets/angular-material-assets/img/icons/ic_email_24px.svg create mode 100644 public/assets/angular-material-assets/img/icons/ic_euro_24px.svg create mode 100644 public/assets/angular-material-assets/img/icons/ic_insert_drive_file_24px.svg create mode 100644 public/assets/angular-material-assets/img/icons/ic_label_24px.svg create mode 100644 public/assets/angular-material-assets/img/icons/ic_launch_24px.svg create mode 100644 public/assets/angular-material-assets/img/icons/ic_menu_24px.svg create mode 100644 public/assets/angular-material-assets/img/icons/ic_more_vert_24px.svg create mode 100644 public/assets/angular-material-assets/img/icons/ic_ondemand_video_24px.svg create mode 100644 public/assets/angular-material-assets/img/icons/ic_people_24px.svg create mode 100644 public/assets/angular-material-assets/img/icons/ic_person_24px.svg create mode 100644 public/assets/angular-material-assets/img/icons/ic_phone_24px.svg create mode 100644 public/assets/angular-material-assets/img/icons/ic_photo_24px.svg create mode 100644 public/assets/angular-material-assets/img/icons/ic_place_24px.svg create mode 100644 public/assets/angular-material-assets/img/icons/ic_play_arrow_24px.svg create mode 100644 public/assets/angular-material-assets/img/icons/ic_play_circle_fill_24px.svg create mode 100644 public/assets/angular-material-assets/img/icons/ic_refresh_24px.svg create mode 100644 public/assets/angular-material-assets/img/icons/ic_school_24px.svg create mode 100644 public/assets/angular-material-assets/img/icons/ic_visibility_24px.svg create mode 100644 public/assets/angular-material-assets/img/icons/launch.svg create mode 100644 public/assets/angular-material-assets/img/icons/list_control_down.png create mode 100644 public/assets/angular-material-assets/img/icons/mail.svg create mode 100644 public/assets/angular-material-assets/img/icons/menu.svg create mode 100644 public/assets/angular-material-assets/img/icons/message.svg create mode 100644 public/assets/angular-material-assets/img/icons/more_vert.svg create mode 100644 public/assets/angular-material-assets/img/icons/npm-logo.svg create mode 100644 public/assets/angular-material-assets/img/icons/octicon-repo.svg create mode 100644 public/assets/angular-material-assets/img/icons/print.svg create mode 100644 public/assets/angular-material-assets/img/icons/separator.svg create mode 100644 public/assets/angular-material-assets/img/icons/sets/communication-icons.svg create mode 100644 public/assets/angular-material-assets/img/icons/sets/core-icons.svg create mode 100644 public/assets/angular-material-assets/img/icons/sets/device-icons.svg create mode 100644 public/assets/angular-material-assets/img/icons/sets/social-icons.svg create mode 100644 public/assets/angular-material-assets/img/icons/share-arrow.svg create mode 100644 public/assets/angular-material-assets/img/icons/tabs-arrow.svg create mode 100644 public/assets/angular-material-assets/img/icons/toggle-arrow.svg create mode 100644 public/assets/angular-material-assets/img/icons/twitter.svg create mode 100644 public/assets/angular-material-assets/img/icons/upload.svg create mode 100644 public/assets/angular-material-assets/img/list/60.jpeg create mode 100644 public/assets/angular-material-assets/img/logo.svg create mode 100644 public/assets/angular-material-assets/img/mangues.jpg create mode 100644 public/assets/angular-material-assets/img/testimonials/logo-bradgreen@2x.fw.png create mode 100644 public/assets/angular-material-assets/img/testimonials/logo-bradgreen@2x.png create mode 100644 public/assets/angular-material-assets/img/testimonials/logo-maxlynch@2x.fw.png create mode 100644 public/assets/angular-material-assets/img/testimonials/logo-maxlynch@2x.png create mode 100644 public/assets/angular-material-assets/img/testimonials/logo-thomasburleson@2x.png create mode 100644 public/assets/angular-material-assets/img/testimonials/quote.png create mode 100644 public/assets/angular-material-assets/img/testimonials/testimonial-hampton@2x.png create mode 100644 public/assets/angular-material-assets/img/testimonials/testimonial-holly@2x.png create mode 100644 public/assets/angular-material-assets/img/testimonials/testimonial-james@2x.png create mode 100644 public/assets/angular-material-assets/img/washedout.png create mode 100644 public/assets/css/custom.css create mode 100644 public/assets/css/index.css create mode 100644 public/assets/css/vendor.css create mode 100644 public/assets/icons/fonts/icomoon.eot create mode 100644 public/assets/icons/fonts/icomoon.svg create mode 100644 public/assets/icons/fonts/icomoon.ttf create mode 100644 public/assets/icons/fonts/icomoon.woff create mode 100644 public/assets/icons/selection.json create mode 100644 public/assets/images/avatars/profile.jpg create mode 100644 public/assets/images/backgrounds/april.jpg create mode 100644 public/assets/images/backgrounds/august.jpg create mode 100644 public/assets/images/backgrounds/december.jpg create mode 100644 public/assets/images/backgrounds/february.jpg create mode 100644 public/assets/images/backgrounds/header-bg.png create mode 100644 public/assets/images/backgrounds/january.jpg create mode 100644 public/assets/images/backgrounds/july.jpg create mode 100644 public/assets/images/backgrounds/june.jpg create mode 100644 public/assets/images/backgrounds/march.jpg create mode 100644 public/assets/images/backgrounds/may.jpg create mode 100644 public/assets/images/backgrounds/november.jpg create mode 100644 public/assets/images/backgrounds/october.jpg create mode 100644 public/assets/images/backgrounds/september.jpg create mode 100644 public/assets/images/business/agents.jpg create mode 100644 public/assets/images/business/companies.jpg create mode 100644 public/assets/images/business/lists.jpg create mode 100644 public/assets/images/business/queues.jpg create mode 100644 public/assets/images/business/telephones.jpg create mode 100644 public/assets/images/business/users.jpg create mode 100644 public/assets/images/flags/ar.png create mode 100644 public/assets/images/flags/da.png create mode 100644 public/assets/images/flags/de.png create mode 100644 public/assets/images/flags/en_EN.png create mode 100644 public/assets/images/flags/es.png create mode 100644 public/assets/images/flags/fa.png create mode 100644 public/assets/images/flags/fi.png create mode 100644 public/assets/images/flags/fr.png create mode 100644 public/assets/images/flags/hi.png create mode 100644 public/assets/images/flags/it.png create mode 100644 public/assets/images/flags/ja.png create mode 100644 public/assets/images/flags/ko.png create mode 100644 public/assets/images/flags/nl.png create mode 100644 public/assets/images/flags/no.png create mode 100644 public/assets/images/flags/pt-BR.png create mode 100644 public/assets/images/flags/pt-PT.png create mode 100644 public/assets/images/flags/ru.png create mode 100644 public/assets/images/flags/sv.png create mode 100644 public/assets/images/flags/tr.png create mode 100644 public/assets/images/flags/us.png create mode 100644 public/assets/images/flags/zh-CN.png create mode 100644 public/assets/images/flags/zh-TW.png create mode 100644 public/assets/images/logos/loading.png create mode 100644 public/assets/images/logos/login.png create mode 100644 public/assets/images/logos/whisker_32x32.png create mode 100644 public/assets/images/music-player/error.png create mode 100644 public/assets/images/music-player/success.png create mode 100644 public/assets/images/music-player/uploading.png create mode 100644 public/assets/images/theme-options/content-only.jpg create mode 100644 public/assets/images/theme-options/content-with-toolbar.jpg create mode 100644 public/assets/images/theme-options/horizontal-nav.jpg create mode 100644 public/assets/images/theme-options/vertical-nav-with-full-toolbar-2.jpg create mode 100644 public/assets/images/theme-options/vertical-nav-with-full-toolbar.jpg create mode 100644 public/assets/images/theme-options/vertical-nav.jpg create mode 100644 public/assets/js/index.min.js create mode 100644 public/assets/js/vendor.js create mode 100644 public/assets/plugins/mxgraph/css/common.css create mode 100644 public/assets/plugins/mxgraph/css/explorer.css create mode 100644 public/assets/plugins/mxgraph/images/button.gif create mode 100644 public/assets/plugins/mxgraph/images/close.gif create mode 100644 public/assets/plugins/mxgraph/images/collapsed.gif create mode 100644 public/assets/plugins/mxgraph/images/error.gif create mode 100644 public/assets/plugins/mxgraph/images/expanded.gif create mode 100644 public/assets/plugins/mxgraph/images/maximize.gif create mode 100644 public/assets/plugins/mxgraph/images/minimize.gif create mode 100644 public/assets/plugins/mxgraph/images/normalize.gif create mode 100644 public/assets/plugins/mxgraph/images/point.gif create mode 100644 public/assets/plugins/mxgraph/images/resize.gif create mode 100644 public/assets/plugins/mxgraph/images/separator.gif create mode 100644 public/assets/plugins/mxgraph/images/submenu.gif create mode 100644 public/assets/plugins/mxgraph/images/transparent.gif create mode 100644 public/assets/plugins/mxgraph/images/warning.gif create mode 100644 public/assets/plugins/mxgraph/images/warning.png create mode 100644 public/assets/plugins/mxgraph/images/window-title.gif create mode 100644 public/assets/plugins/mxgraph/images/window.gif create mode 100644 public/assets/plugins/mxgraph/mxClient.js create mode 100644 public/assets/plugins/square/images/checkmark.gif create mode 100644 public/assets/plugins/square/images/clear.gif create mode 100644 public/assets/plugins/square/images/close.png create mode 100644 public/assets/plugins/square/images/collapsed.gif create mode 100644 public/assets/plugins/square/images/connector.png create mode 100644 public/assets/plugins/square/images/dropdown.gif create mode 100644 public/assets/plugins/square/images/dropdown.png create mode 100644 public/assets/plugins/square/images/edit.gif create mode 100644 public/assets/plugins/square/images/expanded.gif create mode 100644 public/assets/plugins/square/images/grid.gif create mode 100644 public/assets/plugins/square/images/handle-fixed.png create mode 100644 public/assets/plugins/square/images/handle-main.png create mode 100644 public/assets/plugins/square/images/handle-rotate.png create mode 100644 public/assets/plugins/square/images/handle-secondary.png create mode 100644 public/assets/plugins/square/images/handle-terminal.png create mode 100644 public/assets/plugins/square/images/help.png create mode 100644 public/assets/plugins/square/images/locked.png create mode 100644 public/assets/plugins/square/images/logo.png create mode 100644 public/assets/plugins/square/images/nocolor.png create mode 100644 public/assets/plugins/square/images/refresh.png create mode 100644 public/assets/plugins/square/images/round-drop.png create mode 100644 public/assets/plugins/square/images/search.png create mode 100644 public/assets/plugins/square/images/tooltip.png create mode 100644 public/assets/plugins/square/images/transparent.gif create mode 100644 public/assets/plugins/square/images/triangle-down.png create mode 100644 public/assets/plugins/square/images/triangle-left.png create mode 100644 public/assets/plugins/square/images/triangle-right.png create mode 100644 public/assets/plugins/square/images/triangle-up.png create mode 100644 public/assets/plugins/square/images/unlocked.png create mode 100644 public/assets/plugins/square/js/Actions.js create mode 100644 public/assets/plugins/square/js/Dialogs.js create mode 100644 public/assets/plugins/square/js/Editor.js create mode 100644 public/assets/plugins/square/js/EditorUi.js create mode 100644 public/assets/plugins/square/js/Graph.js create mode 100644 public/assets/plugins/square/js/Init.js create mode 100644 public/assets/plugins/square/js/Menus.js create mode 100644 public/assets/plugins/square/js/Sidebar.js create mode 100644 public/assets/plugins/square/js/Toolbar.js create mode 100644 public/assets/plugins/square/resources/grapheditor.txt create mode 100644 public/assets/plugins/square/resources/grapheditor_da.txt create mode 100644 public/assets/plugins/square/resources/grapheditor_de.txt create mode 100644 public/assets/plugins/square/resources/grapheditor_es.txt create mode 100644 public/assets/plugins/square/resources/grapheditor_fa.txt create mode 100644 public/assets/plugins/square/resources/grapheditor_fi.txt create mode 100644 public/assets/plugins/square/resources/grapheditor_fr.txt create mode 100644 public/assets/plugins/square/resources/grapheditor_hi.txt create mode 100644 public/assets/plugins/square/resources/grapheditor_it.txt create mode 100644 public/assets/plugins/square/resources/grapheditor_ja.txt create mode 100644 public/assets/plugins/square/resources/grapheditor_ko.txt create mode 100644 public/assets/plugins/square/resources/grapheditor_nl.txt create mode 100644 public/assets/plugins/square/resources/grapheditor_no.txt create mode 100644 public/assets/plugins/square/resources/grapheditor_pt-PT.txt create mode 100644 public/assets/plugins/square/resources/grapheditor_ru.txt create mode 100644 public/assets/plugins/square/resources/grapheditor_sv.txt create mode 100644 public/assets/plugins/square/resources/grapheditor_tr.txt create mode 100644 public/assets/plugins/square/resources/grapheditor_zh-CN.txt create mode 100644 public/assets/plugins/square/resources/grapheditor_zh-TW.txt create mode 100644 public/assets/plugins/square/resources/help.html create mode 100644 public/assets/plugins/square/resources/help_de.html create mode 100644 public/assets/plugins/square/stencils/call_management/answer_128x128.png create mode 100644 public/assets/plugins/square/stencils/call_management/callback_128x128.png create mode 100644 public/assets/plugins/square/stencils/call_management/dial_128x128.png create mode 100644 public/assets/plugins/square/stencils/call_management/ext_dial_128x128.png create mode 100644 public/assets/plugins/square/stencils/call_management/hangup_128x128.png create mode 100644 public/assets/plugins/square/stencils/call_management/queue_128x128.png create mode 100644 public/assets/plugins/square/stencils/call_management/voicemail_128x128.png create mode 100644 public/assets/plugins/square/stencils/callflow/gotoc_128x128.png create mode 100644 public/assets/plugins/square/stencils/callflow/gotoif_128x128.png create mode 100644 public/assets/plugins/square/stencils/callflow/gotoifmultitime_128x128.png create mode 100644 public/assets/plugins/square/stencils/callflow/gotoiftime_128x128.png create mode 100644 public/assets/plugins/square/stencils/callflow/vswitch_128x128.png create mode 100644 public/assets/plugins/square/stencils/entry/end_128x128.png create mode 100644 public/assets/plugins/square/stencils/entry/finally_128x128.png create mode 100644 public/assets/plugins/square/stencils/entry/start_128x128.png create mode 100644 public/assets/plugins/square/stencils/general.xml create mode 100644 public/assets/plugins/square/stencils/general/agi_128x128.png create mode 100644 public/assets/plugins/square/stencils/general/custom_app_128x128.png create mode 100644 public/assets/plugins/square/stencils/general/noop_128x128.png create mode 100644 public/assets/plugins/square/stencils/general/subproject_128x128.png create mode 100644 public/assets/plugins/square/stencils/general/system_128x128.png create mode 100644 public/assets/plugins/square/stencils/integration_server/asr_128x128.png create mode 100644 public/assets/plugins/square/stencils/integration_server/database_128x128.png create mode 100644 public/assets/plugins/square/stencils/integration_server/ispeechasr_128x128.png create mode 100644 public/assets/plugins/square/stencils/integration_server/sendFax_128x128.png create mode 100644 public/assets/plugins/square/stencils/integration_server/sendMail_128x128.png create mode 100644 public/assets/plugins/square/stencils/integration_server/sendSMS_128x128.png create mode 100644 public/assets/plugins/square/stencils/playback/background_128x128.png create mode 100644 public/assets/plugins/square/stencils/playback/getdigits_128x128.png create mode 100644 public/assets/plugins/square/stencils/playback/getsecretdigits_128x128.png create mode 100644 public/assets/plugins/square/stencils/playback/ispeechtts_128x128.png create mode 100644 public/assets/plugins/square/stencils/playback/menu_128x128.png create mode 100644 public/assets/plugins/square/stencils/playback/playback_128x128.png create mode 100644 public/assets/plugins/square/stencils/playback/saydigits_128x128.png create mode 100644 public/assets/plugins/square/stencils/playback/saynumber_128x128.png create mode 100644 public/assets/plugins/square/stencils/playback/sayphonetic_128x128.png create mode 100644 public/assets/plugins/square/stencils/playback/tts_128x128.png create mode 100644 public/assets/plugins/square/stencils/recording/record_128x128.png create mode 100644 public/assets/plugins/square/stencils/stats/goal_128x128.png create mode 100644 public/assets/plugins/square/stencils/stats/queuelog_128x128.png create mode 100644 public/assets/plugins/square/stencils/variable/math_128x128.png create mode 100644 public/assets/plugins/square/stencils/variable/set_128x128.png create mode 100644 public/assets/plugins/square/styles/default.xml create mode 100644 public/assets/plugins/square/styles/down.gif create mode 100644 public/assets/plugins/square/styles/grapheditor.css create mode 100644 public/assets/plugins/square/styles/help.css create mode 100644 public/assets/plugins/square/styles/sprites.png create mode 100644 public/assets/plugins/square/styles/thumb_horz.png create mode 100644 public/assets/plugins/square/styles/thumb_vertical.png create mode 100644 public/assets/plugins/square/styles/up.gif create mode 100644 public/index.html create mode 100644 server/api/action/action.attributes.js create mode 100644 server/api/action/action.controller.js create mode 100644 server/api/action/action.model.js create mode 100644 server/api/action/action.rpc.js create mode 100644 server/api/action/index.js create mode 100644 server/api/analyticCustomReport/analyticCustomReport.attributes.js create mode 100644 server/api/analyticCustomReport/analyticCustomReport.controller.js create mode 100644 server/api/analyticCustomReport/analyticCustomReport.model.js create mode 100644 server/api/analyticCustomReport/analyticCustomReport.rpc.js create mode 100644 server/api/analyticCustomReport/index.js create mode 100644 server/api/analyticDefaultReport/analyticDefaultReport.attributes.js create mode 100644 server/api/analyticDefaultReport/analyticDefaultReport.controller.js create mode 100644 server/api/analyticDefaultReport/analyticDefaultReport.model.js create mode 100644 server/api/analyticDefaultReport/analyticDefaultReport.rpc.js create mode 100644 server/api/analyticDefaultReport/index.js create mode 100644 server/api/analyticExtractedReport/analyticExtractedReport.attributes.js create mode 100644 server/api/analyticExtractedReport/analyticExtractedReport.controller.js create mode 100644 server/api/analyticExtractedReport/analyticExtractedReport.model.js create mode 100644 server/api/analyticExtractedReport/analyticExtractedReport.rpc.js create mode 100644 server/api/analyticExtractedReport/index.js create mode 100644 server/api/analyticFieldReport/analyticFieldReport.attributes.js create mode 100644 server/api/analyticFieldReport/analyticFieldReport.controller.js create mode 100644 server/api/analyticFieldReport/analyticFieldReport.model.js create mode 100644 server/api/analyticFieldReport/analyticFieldReport.rpc.js create mode 100644 server/api/analyticFieldReport/index.js create mode 100644 server/api/analyticMetric/analyticMetric.attributes.js create mode 100644 server/api/analyticMetric/analyticMetric.controller.js create mode 100644 server/api/analyticMetric/analyticMetric.model.js create mode 100644 server/api/analyticMetric/analyticMetric.rpc.js create mode 100644 server/api/analyticMetric/index.js create mode 100644 server/api/analyticTreeReport/analyticTreeReport.attributes.js create mode 100644 server/api/analyticTreeReport/analyticTreeReport.controller.js create mode 100644 server/api/analyticTreeReport/analyticTreeReport.model.js create mode 100644 server/api/analyticTreeReport/analyticTreeReport.rpc.js create mode 100644 server/api/analyticTreeReport/index.js create mode 100644 server/api/authGoogle/authGoogle.controller.js create mode 100644 server/api/authGoogle/index.js create mode 100644 server/api/authLocal/authLocal.controller.js create mode 100644 server/api/authLocal/index.js create mode 100644 server/api/automation/automation.attributes.js create mode 100644 server/api/automation/automation.controller.js create mode 100644 server/api/automation/automation.model.js create mode 100644 server/api/automation/automation.rpc.js create mode 100644 server/api/automation/index.js create mode 100644 server/api/campaign/campaign.attributes.js create mode 100644 server/api/campaign/campaign.controller.js create mode 100644 server/api/campaign/campaign.model.js create mode 100644 server/api/campaign/campaign.rpc.js create mode 100644 server/api/campaign/index.js create mode 100644 server/api/cannedAnswer/cannedAnswer.attributes.js create mode 100644 server/api/cannedAnswer/cannedAnswer.controller.js create mode 100644 server/api/cannedAnswer/cannedAnswer.model.js create mode 100644 server/api/cannedAnswer/cannedAnswer.rpc.js create mode 100644 server/api/cannedAnswer/index.js create mode 100644 server/api/cdr/cdr.attributes.js create mode 100644 server/api/cdr/cdr.controller.js create mode 100644 server/api/cdr/cdr.model.js create mode 100644 server/api/cdr/cdr.rpc.js create mode 100644 server/api/cdr/index.js create mode 100644 server/api/chatApplication/chatApplication.attributes.js create mode 100644 server/api/chatApplication/chatApplication.controller.js create mode 100644 server/api/chatApplication/chatApplication.model.js create mode 100644 server/api/chatApplication/chatApplication.rpc.js create mode 100644 server/api/chatApplication/index.js create mode 100644 server/api/chatDisposition/chatDisposition.attributes.js create mode 100644 server/api/chatDisposition/chatDisposition.controller.js create mode 100644 server/api/chatDisposition/chatDisposition.model.js create mode 100644 server/api/chatDisposition/chatDisposition.rpc.js create mode 100644 server/api/chatDisposition/index.js create mode 100644 server/api/chatEnquiry/chatEnquiry.attributes.js create mode 100644 server/api/chatEnquiry/chatEnquiry.controller.js create mode 100644 server/api/chatEnquiry/chatEnquiry.model.js create mode 100644 server/api/chatEnquiry/chatEnquiry.rpc.js create mode 100644 server/api/chatEnquiry/index.js create mode 100644 server/api/chatInteraction/chatInteraction.attributes.js create mode 100644 server/api/chatInteraction/chatInteraction.controller.js create mode 100644 server/api/chatInteraction/chatInteraction.model.js create mode 100644 server/api/chatInteraction/chatInteraction.rpc.js create mode 100644 server/api/chatInteraction/index.js create mode 100644 server/api/chatMessage/chatMessage.attributes.js create mode 100644 server/api/chatMessage/chatMessage.controller.js create mode 100644 server/api/chatMessage/chatMessage.model.js create mode 100644 server/api/chatMessage/chatMessage.rpc.js create mode 100644 server/api/chatMessage/index.js create mode 100644 server/api/chatProactiveAction/chatProactiveAction.attributes.js create mode 100644 server/api/chatProactiveAction/chatProactiveAction.controller.js create mode 100644 server/api/chatProactiveAction/chatProactiveAction.model.js create mode 100644 server/api/chatProactiveAction/chatProactiveAction.rpc.js create mode 100644 server/api/chatProactiveAction/index.js create mode 100644 server/api/chatQueue/chatQueue.attributes.js create mode 100644 server/api/chatQueue/chatQueue.controller.js create mode 100644 server/api/chatQueue/chatQueue.model.js create mode 100644 server/api/chatQueue/chatQueue.rpc.js create mode 100644 server/api/chatQueue/index.js create mode 100644 server/api/chatReport/chatReport.attributes.js create mode 100644 server/api/chatReport/chatReport.controller.js create mode 100644 server/api/chatReport/chatReport.model.js create mode 100644 server/api/chatReport/chatReport.rpc.js create mode 100644 server/api/chatReport/index.js create mode 100644 server/api/chatReportHistory/chatReportHistory.attributes.js create mode 100644 server/api/chatReportHistory/chatReportHistory.controller.js create mode 100644 server/api/chatReportHistory/chatReportHistory.model.js create mode 100644 server/api/chatReportHistory/chatReportHistory.rpc.js create mode 100644 server/api/chatReportHistory/index.js create mode 100644 server/api/chatSessionReport/chatSessionReport.attributes.js create mode 100644 server/api/chatSessionReport/chatSessionReport.controller.js create mode 100644 server/api/chatSessionReport/chatSessionReport.model.js create mode 100644 server/api/chatSessionReport/chatSessionReport.rpc.js create mode 100644 server/api/chatSessionReport/index.js create mode 100644 server/api/chatSessionReportHistory/chatSessionReportHistory.attributes.js create mode 100644 server/api/chatSessionReportHistory/chatSessionReportHistory.controller.js create mode 100644 server/api/chatSessionReportHistory/chatSessionReportHistory.model.js create mode 100644 server/api/chatSessionReportHistory/chatSessionReportHistory.rpc.js create mode 100644 server/api/chatSessionReportHistory/index.js create mode 100644 server/api/chatVisitor/chatVisitor.attributes.js create mode 100644 server/api/chatVisitor/chatVisitor.controller.js create mode 100644 server/api/chatVisitor/chatVisitor.model.js create mode 100644 server/api/chatVisitor/chatVisitor.rpc.js create mode 100644 server/api/chatVisitor/index.js create mode 100644 server/api/chatWebsite/chatWebsite.attributes.js create mode 100644 server/api/chatWebsite/chatWebsite.controller.js create mode 100644 server/api/chatWebsite/chatWebsite.model.js create mode 100644 server/api/chatWebsite/chatWebsite.rpc.js create mode 100644 server/api/chatWebsite/index.js create mode 100644 server/api/chatWebsiteField/chatWebsiteField.attributes.js create mode 100644 server/api/chatWebsiteField/chatWebsiteField.controller.js create mode 100644 server/api/chatWebsiteField/chatWebsiteField.model.js create mode 100644 server/api/chatWebsiteField/chatWebsiteField.rpc.js create mode 100644 server/api/chatWebsiteField/index.js create mode 100644 server/api/cmCompany/cmCompany.attributes.js create mode 100644 server/api/cmCompany/cmCompany.controller.js create mode 100644 server/api/cmCompany/cmCompany.model.js create mode 100644 server/api/cmCompany/cmCompany.rpc.js create mode 100644 server/api/cmCompany/index.js create mode 100644 server/api/cmContact/cmContact.attributes.js create mode 100644 server/api/cmContact/cmContact.controller.js create mode 100644 server/api/cmContact/cmContact.model.js create mode 100644 server/api/cmContact/cmContact.rpc.js create mode 100644 server/api/cmContact/index.js create mode 100644 server/api/cmCustomField/cmCustomField.attributes.js create mode 100644 server/api/cmCustomField/cmCustomField.controller.js create mode 100644 server/api/cmCustomField/cmCustomField.model.js create mode 100644 server/api/cmCustomField/cmCustomField.rpc.js create mode 100644 server/api/cmCustomField/index.js create mode 100644 server/api/cmHopper/cmHopper.attributes.js create mode 100644 server/api/cmHopper/cmHopper.controller.js create mode 100644 server/api/cmHopper/cmHopper.model.js create mode 100644 server/api/cmHopper/cmHopper.rpc.js create mode 100644 server/api/cmHopper/index.js create mode 100644 server/api/cmHopperBlack/cmHopperBlack.attributes.js create mode 100644 server/api/cmHopperBlack/cmHopperBlack.controller.js create mode 100644 server/api/cmHopperBlack/cmHopperBlack.model.js create mode 100644 server/api/cmHopperBlack/cmHopperBlack.rpc.js create mode 100644 server/api/cmHopperBlack/index.js create mode 100644 server/api/cmHopperFinal/cmHopperFinal.attributes.js create mode 100644 server/api/cmHopperFinal/cmHopperFinal.controller.js create mode 100644 server/api/cmHopperFinal/cmHopperFinal.model.js create mode 100644 server/api/cmHopperFinal/cmHopperFinal.rpc.js create mode 100644 server/api/cmHopperFinal/index.js create mode 100644 server/api/cmHopperHistory/cmHopperHistory.attributes.js create mode 100644 server/api/cmHopperHistory/cmHopperHistory.controller.js create mode 100644 server/api/cmHopperHistory/cmHopperHistory.model.js create mode 100644 server/api/cmHopperHistory/cmHopperHistory.rpc.js create mode 100644 server/api/cmHopperHistory/index.js create mode 100644 server/api/cmList/cmList.attributes.js create mode 100644 server/api/cmList/cmList.controller.js create mode 100644 server/api/cmList/cmList.model.js create mode 100644 server/api/cmList/cmList.rpc.js create mode 100644 server/api/cmList/index.js create mode 100644 server/api/condition/condition.attributes.js create mode 100644 server/api/condition/condition.controller.js create mode 100644 server/api/condition/condition.model.js create mode 100644 server/api/condition/condition.rpc.js create mode 100644 server/api/condition/index.js create mode 100644 server/api/dashboard/dashboard.attributes.js create mode 100644 server/api/dashboard/dashboard.controller.js create mode 100644 server/api/dashboard/dashboard.model.js create mode 100644 server/api/dashboard/dashboard.rpc.js create mode 100644 server/api/dashboard/index.js create mode 100644 server/api/dashboardItem/dashboardItem.attributes.js create mode 100644 server/api/dashboardItem/dashboardItem.controller.js create mode 100644 server/api/dashboardItem/dashboardItem.model.js create mode 100644 server/api/dashboardItem/dashboardItem.rpc.js create mode 100644 server/api/dashboardItem/index.js create mode 100644 server/api/faxAccount/faxAccount.attributes.js create mode 100644 server/api/faxAccount/faxAccount.controller.js create mode 100644 server/api/faxAccount/faxAccount.model.js create mode 100644 server/api/faxAccount/faxAccount.rpc.js create mode 100644 server/api/faxAccount/index.js create mode 100644 server/api/faxApplication/faxApplication.attributes.js create mode 100644 server/api/faxApplication/faxApplication.controller.js create mode 100644 server/api/faxApplication/faxApplication.model.js create mode 100644 server/api/faxApplication/faxApplication.rpc.js create mode 100644 server/api/faxApplication/index.js create mode 100644 server/api/faxAttachment/faxAttachment.attributes.js create mode 100644 server/api/faxAttachment/faxAttachment.controller.js create mode 100644 server/api/faxAttachment/faxAttachment.model.js create mode 100644 server/api/faxAttachment/faxAttachment.rpc.js create mode 100644 server/api/faxAttachment/index.js create mode 100644 server/api/faxDisposition/faxDisposition.attributes.js create mode 100644 server/api/faxDisposition/faxDisposition.controller.js create mode 100644 server/api/faxDisposition/faxDisposition.model.js create mode 100644 server/api/faxDisposition/faxDisposition.rpc.js create mode 100644 server/api/faxDisposition/index.js create mode 100644 server/api/faxInteraction/faxInteraction.attributes.js create mode 100644 server/api/faxInteraction/faxInteraction.controller.js create mode 100644 server/api/faxInteraction/faxInteraction.model.js create mode 100644 server/api/faxInteraction/faxInteraction.rpc.js create mode 100644 server/api/faxInteraction/index.js create mode 100644 server/api/faxMessage/faxMessage.attributes.js create mode 100644 server/api/faxMessage/faxMessage.controller.js create mode 100644 server/api/faxMessage/faxMessage.model.js create mode 100644 server/api/faxMessage/faxMessage.rpc.js create mode 100644 server/api/faxMessage/index.js create mode 100644 server/api/faxQueue/faxQueue.attributes.js create mode 100644 server/api/faxQueue/faxQueue.controller.js create mode 100644 server/api/faxQueue/faxQueue.model.js create mode 100644 server/api/faxQueue/faxQueue.rpc.js create mode 100644 server/api/faxQueue/index.js create mode 100644 server/api/faxReport/faxReport.attributes.js create mode 100644 server/api/faxReport/faxReport.controller.js create mode 100644 server/api/faxReport/faxReport.model.js create mode 100644 server/api/faxReport/faxReport.rpc.js create mode 100644 server/api/faxReport/index.js create mode 100644 server/api/faxReportHistory/faxReportHistory.attributes.js create mode 100644 server/api/faxReportHistory/faxReportHistory.controller.js create mode 100644 server/api/faxReportHistory/faxReportHistory.model.js create mode 100644 server/api/faxReportHistory/faxReportHistory.rpc.js create mode 100644 server/api/faxReportHistory/index.js create mode 100644 server/api/faxSessionReport/faxSessionReport.attributes.js create mode 100644 server/api/faxSessionReport/faxSessionReport.controller.js create mode 100644 server/api/faxSessionReport/faxSessionReport.model.js create mode 100644 server/api/faxSessionReport/faxSessionReport.rpc.js create mode 100644 server/api/faxSessionReport/index.js create mode 100644 server/api/faxSessionReportHistory/faxSessionReportHistory.attributes.js create mode 100644 server/api/faxSessionReportHistory/faxSessionReportHistory.controller.js create mode 100644 server/api/faxSessionReportHistory/faxSessionReportHistory.model.js create mode 100644 server/api/faxSessionReportHistory/faxSessionReportHistory.rpc.js create mode 100644 server/api/faxSessionReportHistory/index.js create mode 100644 server/api/intFreshdeskAccount/index.js create mode 100644 server/api/intFreshdeskAccount/intFreshdeskAccount.attributes.js create mode 100644 server/api/intFreshdeskAccount/intFreshdeskAccount.controller.js create mode 100644 server/api/intFreshdeskAccount/intFreshdeskAccount.model.js create mode 100644 server/api/intFreshdeskAccount/intFreshdeskAccount.rpc.js create mode 100644 server/api/intFreshdeskConfiguration/index.js create mode 100644 server/api/intFreshdeskConfiguration/intFreshdeskConfiguration.attributes.js create mode 100644 server/api/intFreshdeskConfiguration/intFreshdeskConfiguration.controller.js create mode 100644 server/api/intFreshdeskConfiguration/intFreshdeskConfiguration.model.js create mode 100644 server/api/intFreshdeskConfiguration/intFreshdeskConfiguration.rpc.js create mode 100644 server/api/intFreshdeskField/index.js create mode 100644 server/api/intFreshdeskField/intFreshdeskField.attributes.js create mode 100644 server/api/intFreshdeskField/intFreshdeskField.controller.js create mode 100644 server/api/intFreshdeskField/intFreshdeskField.model.js create mode 100644 server/api/intFreshdeskField/intFreshdeskField.rpc.js create mode 100644 server/api/intSalesforceAccount/index.js create mode 100644 server/api/intSalesforceAccount/intSalesforceAccount.attributes.js create mode 100644 server/api/intSalesforceAccount/intSalesforceAccount.controller.js create mode 100644 server/api/intSalesforceAccount/intSalesforceAccount.model.js create mode 100644 server/api/intSalesforceAccount/intSalesforceAccount.rpc.js create mode 100644 server/api/intSalesforceConfiguration/index.js create mode 100644 server/api/intSalesforceConfiguration/intSalesforceConfiguration.attributes.js create mode 100644 server/api/intSalesforceConfiguration/intSalesforceConfiguration.controller.js create mode 100644 server/api/intSalesforceConfiguration/intSalesforceConfiguration.model.js create mode 100644 server/api/intSalesforceConfiguration/intSalesforceConfiguration.rpc.js create mode 100644 server/api/intSalesforceField/index.js create mode 100644 server/api/intSalesforceField/intSalesforceField.attributes.js create mode 100644 server/api/intSalesforceField/intSalesforceField.controller.js create mode 100644 server/api/intSalesforceField/intSalesforceField.model.js create mode 100644 server/api/intSalesforceField/intSalesforceField.rpc.js create mode 100644 server/api/intSugarcrmAccount/index.js create mode 100644 server/api/intSugarcrmAccount/intSugarcrmAccount.attributes.js create mode 100644 server/api/intSugarcrmAccount/intSugarcrmAccount.controller.js create mode 100644 server/api/intSugarcrmAccount/intSugarcrmAccount.model.js create mode 100644 server/api/intSugarcrmAccount/intSugarcrmAccount.rpc.js create mode 100644 server/api/intSugarcrmConfiguration/index.js create mode 100644 server/api/intSugarcrmConfiguration/intSugarcrmConfiguration.attributes.js create mode 100644 server/api/intSugarcrmConfiguration/intSugarcrmConfiguration.controller.js create mode 100644 server/api/intSugarcrmConfiguration/intSugarcrmConfiguration.model.js create mode 100644 server/api/intSugarcrmConfiguration/intSugarcrmConfiguration.rpc.js create mode 100644 server/api/intSugarcrmField/index.js create mode 100644 server/api/intSugarcrmField/intSugarcrmField.attributes.js create mode 100644 server/api/intSugarcrmField/intSugarcrmField.controller.js create mode 100644 server/api/intSugarcrmField/intSugarcrmField.model.js create mode 100644 server/api/intSugarcrmField/intSugarcrmField.rpc.js create mode 100644 server/api/intZendeskAccount/index.js create mode 100644 server/api/intZendeskAccount/intZendeskAccount.attributes.js create mode 100644 server/api/intZendeskAccount/intZendeskAccount.controller.js create mode 100644 server/api/intZendeskAccount/intZendeskAccount.model.js create mode 100644 server/api/intZendeskAccount/intZendeskAccount.rpc.js create mode 100644 server/api/intZendeskConfiguration/index.js create mode 100644 server/api/intZendeskConfiguration/intZendeskConfiguration.attributes.js create mode 100644 server/api/intZendeskConfiguration/intZendeskConfiguration.controller.js create mode 100644 server/api/intZendeskConfiguration/intZendeskConfiguration.model.js create mode 100644 server/api/intZendeskConfiguration/intZendeskConfiguration.rpc.js create mode 100644 server/api/intZendeskField/index.js create mode 100644 server/api/intZendeskField/intZendeskField.attributes.js create mode 100644 server/api/intZendeskField/intZendeskField.controller.js create mode 100644 server/api/intZendeskField/intZendeskField.model.js create mode 100644 server/api/intZendeskField/intZendeskField.rpc.js create mode 100644 server/api/integration/index.js create mode 100644 server/api/integration/integration.attributes.js create mode 100644 server/api/integration/integration.controller.js create mode 100644 server/api/integration/integration.model.js create mode 100644 server/api/integration/integration.rpc.js create mode 100644 server/api/integrationReport/index.js create mode 100644 server/api/integrationReport/integrationReport.attributes.js create mode 100644 server/api/integrationReport/integrationReport.controller.js create mode 100644 server/api/integrationReport/integrationReport.model.js create mode 100644 server/api/integrationReport/integrationReport.rpc.js create mode 100644 server/api/integrationReportHistory/index.js create mode 100644 server/api/integrationReportHistory/integrationReportHistory.attributes.js create mode 100644 server/api/integrationReportHistory/integrationReportHistory.controller.js create mode 100644 server/api/integrationReportHistory/integrationReportHistory.model.js create mode 100644 server/api/integrationReportHistory/integrationReportHistory.rpc.js create mode 100644 server/api/interval/index.js create mode 100644 server/api/interval/interval.attributes.js create mode 100644 server/api/interval/interval.controller.js create mode 100644 server/api/interval/interval.model.js create mode 100644 server/api/interval/interval.rpc.js create mode 100644 server/api/jira/index.js create mode 100644 server/api/jira/jira.controller.js create mode 100644 server/api/jscriptyInputReport/index.js create mode 100644 server/api/jscriptyInputReport/jscriptyInputReport.attributes.js create mode 100644 server/api/jscriptyInputReport/jscriptyInputReport.controller.js create mode 100644 server/api/jscriptyInputReport/jscriptyInputReport.model.js create mode 100644 server/api/jscriptyInputReport/jscriptyInputReport.rpc.js create mode 100644 server/api/jscriptyProject/index.js create mode 100644 server/api/jscriptyProject/jscriptyProject.attributes.js create mode 100644 server/api/jscriptyProject/jscriptyProject.controller.js create mode 100644 server/api/jscriptyProject/jscriptyProject.model.js create mode 100644 server/api/jscriptyProject/jscriptyProject.rpc.js create mode 100644 server/api/jscriptyQuestionReport/index.js create mode 100644 server/api/jscriptyQuestionReport/jscriptyQuestionReport.attributes.js create mode 100644 server/api/jscriptyQuestionReport/jscriptyQuestionReport.controller.js create mode 100644 server/api/jscriptyQuestionReport/jscriptyQuestionReport.model.js create mode 100644 server/api/jscriptyQuestionReport/jscriptyQuestionReport.rpc.js create mode 100644 server/api/jscriptySessionReport/index.js create mode 100644 server/api/jscriptySessionReport/jscriptySessionReport.attributes.js create mode 100644 server/api/jscriptySessionReport/jscriptySessionReport.controller.js create mode 100644 server/api/jscriptySessionReport/jscriptySessionReport.model.js create mode 100644 server/api/jscriptySessionReport/jscriptySessionReport.rpc.js create mode 100644 server/api/license/index.js create mode 100644 server/api/license/license.attributes.js create mode 100644 server/api/license/license.controller.js create mode 100644 server/api/license/license.model.js create mode 100644 server/api/license/license.rpc.js create mode 100644 server/api/mailAccount/index.js create mode 100644 server/api/mailAccount/mailAccount.attributes.js create mode 100644 server/api/mailAccount/mailAccount.controller.js create mode 100644 server/api/mailAccount/mailAccount.model.js create mode 100644 server/api/mailAccount/mailAccount.rpc.js create mode 100644 server/api/mailApplication/index.js create mode 100644 server/api/mailApplication/mailApplication.attributes.js create mode 100644 server/api/mailApplication/mailApplication.controller.js create mode 100644 server/api/mailApplication/mailApplication.model.js create mode 100644 server/api/mailApplication/mailApplication.rpc.js create mode 100644 server/api/mailAttachment/index.js create mode 100644 server/api/mailAttachment/mailAttachment.attributes.js create mode 100644 server/api/mailAttachment/mailAttachment.controller.js create mode 100644 server/api/mailAttachment/mailAttachment.model.js create mode 100644 server/api/mailAttachment/mailAttachment.rpc.js create mode 100644 server/api/mailDisposition/index.js create mode 100644 server/api/mailDisposition/mailDisposition.attributes.js create mode 100644 server/api/mailDisposition/mailDisposition.controller.js create mode 100644 server/api/mailDisposition/mailDisposition.model.js create mode 100644 server/api/mailDisposition/mailDisposition.rpc.js create mode 100644 server/api/mailInteraction/index.js create mode 100644 server/api/mailInteraction/mailInteraction.attributes.js create mode 100644 server/api/mailInteraction/mailInteraction.controller.js create mode 100644 server/api/mailInteraction/mailInteraction.model.js create mode 100644 server/api/mailInteraction/mailInteraction.rpc.js create mode 100644 server/api/mailMessage/index.js create mode 100644 server/api/mailMessage/mailMessage.attributes.js create mode 100644 server/api/mailMessage/mailMessage.controller.js create mode 100644 server/api/mailMessage/mailMessage.model.js create mode 100644 server/api/mailMessage/mailMessage.rpc.js create mode 100644 server/api/mailQueue/index.js create mode 100644 server/api/mailQueue/mailQueue.attributes.js create mode 100644 server/api/mailQueue/mailQueue.controller.js create mode 100644 server/api/mailQueue/mailQueue.model.js create mode 100644 server/api/mailQueue/mailQueue.rpc.js create mode 100644 server/api/mailReport/index.js create mode 100644 server/api/mailReport/mailReport.attributes.js create mode 100644 server/api/mailReport/mailReport.controller.js create mode 100644 server/api/mailReport/mailReport.model.js create mode 100644 server/api/mailReport/mailReport.rpc.js create mode 100644 server/api/mailReportHistory/index.js create mode 100644 server/api/mailReportHistory/mailReportHistory.attributes.js create mode 100644 server/api/mailReportHistory/mailReportHistory.controller.js create mode 100644 server/api/mailReportHistory/mailReportHistory.model.js create mode 100644 server/api/mailReportHistory/mailReportHistory.rpc.js create mode 100644 server/api/mailServerIn/index.js create mode 100644 server/api/mailServerIn/mailServerIn.attributes.js create mode 100644 server/api/mailServerIn/mailServerIn.model.js create mode 100644 server/api/mailServerIn/mailServerIn.rpc.js create mode 100644 server/api/mailServerOut/index.js create mode 100644 server/api/mailServerOut/mailServerOut.attributes.js create mode 100644 server/api/mailServerOut/mailServerOut.controller.js create mode 100644 server/api/mailServerOut/mailServerOut.model.js create mode 100644 server/api/mailServerOut/mailServerOut.rpc.js create mode 100644 server/api/mailSessionReport/index.js create mode 100644 server/api/mailSessionReport/mailSessionReport.attributes.js create mode 100644 server/api/mailSessionReport/mailSessionReport.controller.js create mode 100644 server/api/mailSessionReport/mailSessionReport.model.js create mode 100644 server/api/mailSessionReport/mailSessionReport.rpc.js create mode 100644 server/api/mailSessionReportHistory/index.js create mode 100644 server/api/mailSessionReportHistory/mailSessionReportHistory.attributes.js create mode 100644 server/api/mailSessionReportHistory/mailSessionReportHistory.controller.js create mode 100644 server/api/mailSessionReportHistory/mailSessionReportHistory.model.js create mode 100644 server/api/mailSessionReportHistory/mailSessionReportHistory.rpc.js create mode 100644 server/api/memberReport/index.js create mode 100644 server/api/memberReport/memberReport.attributes.js create mode 100644 server/api/memberReport/memberReport.controller.js create mode 100644 server/api/memberReport/memberReport.model.js create mode 100644 server/api/memberReport/memberReport.rpc.js create mode 100644 server/api/memberReportHistory/index.js create mode 100644 server/api/memberReportHistory/memberReportHistory.attributes.js create mode 100644 server/api/memberReportHistory/memberReportHistory.controller.js create mode 100644 server/api/memberReportHistory/memberReportHistory.model.js create mode 100644 server/api/memberReportHistory/memberReportHistory.rpc.js create mode 100644 server/api/module/index.js create mode 100644 server/api/module/module.attributes.js create mode 100644 server/api/module/module.model.js create mode 100644 server/api/module/module.rpc.js create mode 100644 server/api/network/index.js create mode 100644 server/api/network/network.attributes.js create mode 100644 server/api/network/network.controller.js create mode 100644 server/api/network/network.model.js create mode 100644 server/api/network/network.rpc.js create mode 100644 server/api/openchannelAccount/index.js create mode 100644 server/api/openchannelAccount/openchannelAccount.attributes.js create mode 100644 server/api/openchannelAccount/openchannelAccount.controller.js create mode 100644 server/api/openchannelAccount/openchannelAccount.model.js create mode 100644 server/api/openchannelAccount/openchannelAccount.rpc.js create mode 100644 server/api/openchannelApplication/index.js create mode 100644 server/api/openchannelApplication/openchannelApplication.attributes.js create mode 100644 server/api/openchannelApplication/openchannelApplication.controller.js create mode 100644 server/api/openchannelApplication/openchannelApplication.model.js create mode 100644 server/api/openchannelApplication/openchannelApplication.rpc.js create mode 100644 server/api/openchannelDisposition/index.js create mode 100644 server/api/openchannelDisposition/openchannelDisposition.attributes.js create mode 100644 server/api/openchannelDisposition/openchannelDisposition.controller.js create mode 100644 server/api/openchannelDisposition/openchannelDisposition.model.js create mode 100644 server/api/openchannelDisposition/openchannelDisposition.rpc.js create mode 100644 server/api/openchannelInteraction/index.js create mode 100644 server/api/openchannelInteraction/openchannelInteraction.attributes.js create mode 100644 server/api/openchannelInteraction/openchannelInteraction.controller.js create mode 100644 server/api/openchannelInteraction/openchannelInteraction.model.js create mode 100644 server/api/openchannelInteraction/openchannelInteraction.rpc.js create mode 100644 server/api/openchannelMessage/index.js create mode 100644 server/api/openchannelMessage/openchannelMessage.attributes.js create mode 100644 server/api/openchannelMessage/openchannelMessage.controller.js create mode 100644 server/api/openchannelMessage/openchannelMessage.model.js create mode 100644 server/api/openchannelMessage/openchannelMessage.rpc.js create mode 100644 server/api/openchannelQueue/index.js create mode 100644 server/api/openchannelQueue/openchannelQueue.attributes.js create mode 100644 server/api/openchannelQueue/openchannelQueue.controller.js create mode 100644 server/api/openchannelQueue/openchannelQueue.model.js create mode 100644 server/api/openchannelQueue/openchannelQueue.rpc.js create mode 100644 server/api/openchannelReport/index.js create mode 100644 server/api/openchannelReport/openchannelReport.attributes.js create mode 100644 server/api/openchannelReport/openchannelReport.controller.js create mode 100644 server/api/openchannelReport/openchannelReport.model.js create mode 100644 server/api/openchannelReport/openchannelReport.rpc.js create mode 100644 server/api/openchannelReportHistory/index.js create mode 100644 server/api/openchannelReportHistory/openchannelReportHistory.attributes.js create mode 100644 server/api/openchannelReportHistory/openchannelReportHistory.controller.js create mode 100644 server/api/openchannelReportHistory/openchannelReportHistory.model.js create mode 100644 server/api/openchannelReportHistory/openchannelReportHistory.rpc.js create mode 100644 server/api/openchannelSessionReport/index.js create mode 100644 server/api/openchannelSessionReport/openchannelSessionReport.attributes.js create mode 100644 server/api/openchannelSessionReport/openchannelSessionReport.controller.js create mode 100644 server/api/openchannelSessionReport/openchannelSessionReport.model.js create mode 100644 server/api/openchannelSessionReport/openchannelSessionReport.rpc.js create mode 100644 server/api/openchannelSessionReportHistory/index.js create mode 100644 server/api/openchannelSessionReportHistory/openchannelSessionReportHistory.attributes.js create mode 100644 server/api/openchannelSessionReportHistory/openchannelSessionReportHistory.controller.js create mode 100644 server/api/openchannelSessionReportHistory/openchannelSessionReportHistory.model.js create mode 100644 server/api/openchannelSessionReportHistory/openchannelSessionReportHistory.rpc.js create mode 100644 server/api/pause/index.js create mode 100644 server/api/pause/pause.attributes.js create mode 100644 server/api/pause/pause.controller.js create mode 100644 server/api/pause/pause.model.js create mode 100644 server/api/pause/pause.rpc.js create mode 100644 server/api/pm2/index.js create mode 100644 server/api/pm2/pm2.controller.js create mode 100644 server/api/rpc/index.js create mode 100644 server/api/rpc/rpc.controller.js create mode 100644 server/api/schedule/index.js create mode 100644 server/api/schedule/schedule.attributes.js create mode 100644 server/api/schedule/schedule.controller.js create mode 100644 server/api/schedule/schedule.model.js create mode 100644 server/api/schedule/schedule.rpc.js create mode 100644 server/api/setting/index.js create mode 100644 server/api/setting/setting.attributes.js create mode 100644 server/api/setting/setting.controller.js create mode 100644 server/api/setting/setting.model.js create mode 100644 server/api/setting/setting.rpc.js create mode 100644 server/api/smsAccount/index.js create mode 100644 server/api/smsAccount/smsAccount.attributes.js create mode 100644 server/api/smsAccount/smsAccount.controller.js create mode 100644 server/api/smsAccount/smsAccount.model.js create mode 100644 server/api/smsAccount/smsAccount.rpc.js create mode 100644 server/api/smsApplication/index.js create mode 100644 server/api/smsApplication/smsApplication.attributes.js create mode 100644 server/api/smsApplication/smsApplication.controller.js create mode 100644 server/api/smsApplication/smsApplication.model.js create mode 100644 server/api/smsApplication/smsApplication.rpc.js create mode 100644 server/api/smsDisposition/index.js create mode 100644 server/api/smsDisposition/smsDisposition.attributes.js create mode 100644 server/api/smsDisposition/smsDisposition.controller.js create mode 100644 server/api/smsDisposition/smsDisposition.model.js create mode 100644 server/api/smsDisposition/smsDisposition.rpc.js create mode 100644 server/api/smsInteraction/index.js create mode 100644 server/api/smsInteraction/smsInteraction.attributes.js create mode 100644 server/api/smsInteraction/smsInteraction.controller.js create mode 100644 server/api/smsInteraction/smsInteraction.model.js create mode 100644 server/api/smsInteraction/smsInteraction.rpc.js create mode 100644 server/api/smsMessage/index.js create mode 100644 server/api/smsMessage/smsMessage.attributes.js create mode 100644 server/api/smsMessage/smsMessage.controller.js create mode 100644 server/api/smsMessage/smsMessage.model.js create mode 100644 server/api/smsMessage/smsMessage.rpc.js create mode 100644 server/api/smsQueue/index.js create mode 100644 server/api/smsQueue/smsQueue.attributes.js create mode 100644 server/api/smsQueue/smsQueue.controller.js create mode 100644 server/api/smsQueue/smsQueue.model.js create mode 100644 server/api/smsQueue/smsQueue.rpc.js create mode 100644 server/api/smsReport/index.js create mode 100644 server/api/smsReport/smsReport.attributes.js create mode 100644 server/api/smsReport/smsReport.controller.js create mode 100644 server/api/smsReport/smsReport.model.js create mode 100644 server/api/smsReport/smsReport.rpc.js create mode 100644 server/api/smsReportHistory/index.js create mode 100644 server/api/smsReportHistory/smsReportHistory.attributes.js create mode 100644 server/api/smsReportHistory/smsReportHistory.controller.js create mode 100644 server/api/smsReportHistory/smsReportHistory.model.js create mode 100644 server/api/smsReportHistory/smsReportHistory.rpc.js create mode 100644 server/api/smsSessionReport/index.js create mode 100644 server/api/smsSessionReport/smsSessionReport.attributes.js create mode 100644 server/api/smsSessionReport/smsSessionReport.controller.js create mode 100644 server/api/smsSessionReport/smsSessionReport.model.js create mode 100644 server/api/smsSessionReport/smsSessionReport.rpc.js create mode 100644 server/api/smsSessionReportHistory/index.js create mode 100644 server/api/smsSessionReportHistory/smsSessionReportHistory.attributes.js create mode 100644 server/api/smsSessionReportHistory/smsSessionReportHistory.controller.js create mode 100644 server/api/smsSessionReportHistory/smsSessionReportHistory.model.js create mode 100644 server/api/smsSessionReportHistory/smsSessionReportHistory.rpc.js create mode 100644 server/api/sound/index.js create mode 100644 server/api/sound/sound.attributes.js create mode 100644 server/api/sound/sound.controller.js create mode 100644 server/api/sound/sound.model.js create mode 100644 server/api/sound/sound.rpc.js create mode 100644 server/api/squareOdbc/index.js create mode 100644 server/api/squareOdbc/squareOdbc.attributes.js create mode 100644 server/api/squareOdbc/squareOdbc.controller.js create mode 100644 server/api/squareOdbc/squareOdbc.model.js create mode 100644 server/api/squareOdbc/squareOdbc.rpc.js create mode 100644 server/api/squareProject/index.js create mode 100644 server/api/squareProject/squareProject.attributes.js create mode 100644 server/api/squareProject/squareProject.controller.js create mode 100644 server/api/squareProject/squareProject.model.js create mode 100644 server/api/squareProject/squareProject.rpc.js create mode 100644 server/api/squareRecording/index.js create mode 100644 server/api/squareRecording/squareRecording.attributes.js create mode 100644 server/api/squareRecording/squareRecording.controller.js create mode 100644 server/api/squareRecording/squareRecording.model.js create mode 100644 server/api/squareRecording/squareRecording.rpc.js create mode 100644 server/api/squareReport/index.js create mode 100644 server/api/squareReport/squareReport.attributes.js create mode 100644 server/api/squareReport/squareReport.controller.js create mode 100644 server/api/squareReport/squareReport.model.js create mode 100644 server/api/squareReport/squareReport.rpc.js create mode 100644 server/api/squareReportDetail/index.js create mode 100644 server/api/squareReportDetail/squareReportDetail.attributes.js create mode 100644 server/api/squareReportDetail/squareReportDetail.controller.js create mode 100644 server/api/squareReportDetail/squareReportDetail.model.js create mode 100644 server/api/squareReportDetail/squareReportDetail.rpc.js create mode 100644 server/api/squareReportDetailHistory/index.js create mode 100644 server/api/squareReportDetailHistory/squareReportDetailHistory.attributes.js create mode 100644 server/api/squareReportDetailHistory/squareReportDetailHistory.controller.js create mode 100644 server/api/squareReportDetailHistory/squareReportDetailHistory.model.js create mode 100644 server/api/squareReportDetailHistory/squareReportDetailHistory.rpc.js create mode 100644 server/api/squareReportHistory/index.js create mode 100644 server/api/squareReportHistory/squareReportHistory.attributes.js create mode 100644 server/api/squareReportHistory/squareReportHistory.controller.js create mode 100644 server/api/squareReportHistory/squareReportHistory.model.js create mode 100644 server/api/squareReportHistory/squareReportHistory.rpc.js create mode 100644 server/api/system/index.js create mode 100644 server/api/system/system.controller.js create mode 100644 server/api/tag/index.js create mode 100644 server/api/tag/tag.attributes.js create mode 100644 server/api/tag/tag.controller.js create mode 100644 server/api/tag/tag.model.js create mode 100644 server/api/tag/tag.rpc.js create mode 100644 server/api/team/index.js create mode 100644 server/api/team/team.attributes.js create mode 100644 server/api/team/team.controller.js create mode 100644 server/api/team/team.model.js create mode 100644 server/api/team/team.rpc.js create mode 100644 server/api/template/index.js create mode 100644 server/api/template/template.attributes.js create mode 100644 server/api/template/template.controller.js create mode 100644 server/api/template/template.model.js create mode 100644 server/api/template/template.rpc.js create mode 100644 server/api/tigerDialReport/index.js create mode 100644 server/api/tigerDialReport/tigerDialReport.attributes.js create mode 100644 server/api/tigerDialReport/tigerDialReport.controller.js create mode 100644 server/api/tigerDialReport/tigerDialReport.model.js create mode 100644 server/api/tigerDialReport/tigerDialReport.rpc.js create mode 100644 server/api/trigger/index.js create mode 100644 server/api/trigger/trigger.attributes.js create mode 100644 server/api/trigger/trigger.controller.js create mode 100644 server/api/trigger/trigger.model.js create mode 100644 server/api/trigger/trigger.rpc.js create mode 100644 server/api/trunk/index.js create mode 100644 server/api/trunk/trunk.attributes.js create mode 100644 server/api/trunk/trunk.controller.js create mode 100644 server/api/trunk/trunk.model.js create mode 100644 server/api/trunk/trunk.rpc.js create mode 100644 server/api/user/index.js create mode 100644 server/api/user/user.attributes.js create mode 100644 server/api/user/user.controller.js create mode 100644 server/api/user/user.events.js create mode 100644 server/api/user/user.model.js create mode 100644 server/api/user/user.rpc.js create mode 100644 server/api/user/user.socket.js create mode 100644 server/api/userChatQueue/index.js create mode 100644 server/api/userChatQueue/userChatQueue.attributes.js create mode 100644 server/api/userChatQueue/userChatQueue.model.js create mode 100644 server/api/userChatQueue/userChatQueue.rpc.js create mode 100644 server/api/userFaxQueue/index.js create mode 100644 server/api/userFaxQueue/userFaxQueue.attributes.js create mode 100644 server/api/userFaxQueue/userFaxQueue.model.js create mode 100644 server/api/userFaxQueue/userFaxQueue.rpc.js create mode 100644 server/api/userList/index.js create mode 100644 server/api/userList/userList.attributes.js create mode 100644 server/api/userList/userList.model.js create mode 100644 server/api/userList/userList.rpc.js create mode 100644 server/api/userMailQueue/index.js create mode 100644 server/api/userMailQueue/userMailQueue.attributes.js create mode 100644 server/api/userMailQueue/userMailQueue.model.js create mode 100644 server/api/userMailQueue/userMailQueue.rpc.js create mode 100644 server/api/userModule/index.js create mode 100644 server/api/userModule/userModule.attributes.js create mode 100644 server/api/userModule/userModule.model.js create mode 100644 server/api/userModule/userModule.rpc.js create mode 100644 server/api/userOpenchannelQueue/index.js create mode 100644 server/api/userOpenchannelQueue/userOpenchannelQueue.attributes.js create mode 100644 server/api/userOpenchannelQueue/userOpenchannelQueue.model.js create mode 100644 server/api/userOpenchannelQueue/userOpenchannelQueue.rpc.js create mode 100644 server/api/userSmsQueue/index.js create mode 100644 server/api/userSmsQueue/userSmsQueue.attributes.js create mode 100644 server/api/userSmsQueue/userSmsQueue.model.js create mode 100644 server/api/userSmsQueue/userSmsQueue.rpc.js create mode 100644 server/api/userVoiceQueue/index.js create mode 100644 server/api/userVoiceQueue/userVoiceQueue.attributes.js create mode 100644 server/api/userVoiceQueue/userVoiceQueue.model.js create mode 100644 server/api/userVoiceQueue/userVoiceQueue.rpc.js create mode 100644 server/api/userVoiceQueueRt/index.js create mode 100644 server/api/userVoiceQueueRt/userVoiceQueueRt.attributes.js create mode 100644 server/api/userVoiceQueueRt/userVoiceQueueRt.events.js create mode 100644 server/api/userVoiceQueueRt/userVoiceQueueRt.model.js create mode 100644 server/api/userVoiceQueueRt/userVoiceQueueRt.rpc.js create mode 100644 server/api/userVoiceQueueRt/userVoiceQueueRt.socket.js create mode 100644 server/api/variable/index.js create mode 100644 server/api/variable/variable.attributes.js create mode 100644 server/api/variable/variable.controller.js create mode 100644 server/api/variable/variable.model.js create mode 100644 server/api/variable/variable.rpc.js create mode 100644 server/api/version/index.js create mode 100644 server/api/version/version.controller.js create mode 100644 server/api/voiceAgentReport/index.js create mode 100644 server/api/voiceAgentReport/voiceAgentReport.attributes.js create mode 100644 server/api/voiceAgentReport/voiceAgentReport.controller.js create mode 100644 server/api/voiceAgentReport/voiceAgentReport.model.js create mode 100644 server/api/voiceAgentReport/voiceAgentReport.rpc.js create mode 100644 server/api/voiceAgentReportHistory/index.js create mode 100644 server/api/voiceAgentReportHistory/voiceAgentReportHistory.attributes.js create mode 100644 server/api/voiceAgentReportHistory/voiceAgentReportHistory.controller.js create mode 100644 server/api/voiceAgentReportHistory/voiceAgentReportHistory.model.js create mode 100644 server/api/voiceAgentReportHistory/voiceAgentReportHistory.rpc.js create mode 100644 server/api/voiceCallReport/index.js create mode 100644 server/api/voiceCallReport/voiceCallReport.attributes.js create mode 100644 server/api/voiceCallReport/voiceCallReport.controller.js create mode 100644 server/api/voiceCallReport/voiceCallReport.model.js create mode 100644 server/api/voiceCallReport/voiceCallReport.rpc.js create mode 100644 server/api/voiceCallReportHistory/index.js create mode 100644 server/api/voiceCallReportHistory/voiceCallReportHistory.attributes.js create mode 100644 server/api/voiceCallReportHistory/voiceCallReportHistory.controller.js create mode 100644 server/api/voiceCallReportHistory/voiceCallReportHistory.model.js create mode 100644 server/api/voiceCallReportHistory/voiceCallReportHistory.rpc.js create mode 100644 server/api/voiceChanSpy/index.js create mode 100644 server/api/voiceChanSpy/voiceChanSpy.attributes.js create mode 100644 server/api/voiceChanSpy/voiceChanSpy.controller.js create mode 100644 server/api/voiceChanSpy/voiceChanSpy.model.js create mode 100644 server/api/voiceChanSpy/voiceChanSpy.rpc.js create mode 100644 server/api/voiceContext/index.js create mode 100644 server/api/voiceContext/voiceContext.attributes.js create mode 100644 server/api/voiceContext/voiceContext.controller.js create mode 100644 server/api/voiceContext/voiceContext.model.js create mode 100644 server/api/voiceContext/voiceContext.rpc.js create mode 100644 server/api/voiceDialReport/index.js create mode 100644 server/api/voiceDialReport/voiceDialReport.attributes.js create mode 100644 server/api/voiceDialReport/voiceDialReport.controller.js create mode 100644 server/api/voiceDialReport/voiceDialReport.model.js create mode 100644 server/api/voiceDialReport/voiceDialReport.rpc.js create mode 100644 server/api/voiceDialReportHistory/index.js create mode 100644 server/api/voiceDialReportHistory/voiceDialReportHistory.attributes.js create mode 100644 server/api/voiceDialReportHistory/voiceDialReportHistory.controller.js create mode 100644 server/api/voiceDialReportHistory/voiceDialReportHistory.model.js create mode 100644 server/api/voiceDialReportHistory/voiceDialReportHistory.rpc.js create mode 100644 server/api/voiceDisposition/index.js create mode 100644 server/api/voiceDisposition/voiceDisposition.attributes.js create mode 100644 server/api/voiceDisposition/voiceDisposition.controller.js create mode 100644 server/api/voiceDisposition/voiceDisposition.model.js create mode 100644 server/api/voiceDisposition/voiceDisposition.rpc.js create mode 100644 server/api/voiceExtension/index.js create mode 100644 server/api/voiceExtension/voiceExtension.attributes.js create mode 100644 server/api/voiceExtension/voiceExtension.controller.js create mode 100644 server/api/voiceExtension/voiceExtension.model.js create mode 100644 server/api/voiceExtension/voiceExtension.rpc.js create mode 100644 server/api/voiceMail/index.js create mode 100644 server/api/voiceMail/voiceMail.attributes.js create mode 100644 server/api/voiceMail/voiceMail.controller.js create mode 100644 server/api/voiceMail/voiceMail.model.js create mode 100644 server/api/voiceMail/voiceMail.rpc.js create mode 100644 server/api/voiceMailMessage/index.js create mode 100644 server/api/voiceMailMessage/voiceMailMessage.attributes.js create mode 100644 server/api/voiceMailMessage/voiceMailMessage.controller.js create mode 100644 server/api/voiceMailMessage/voiceMailMessage.model.js create mode 100644 server/api/voiceMailMessage/voiceMailMessage.rpc.js create mode 100644 server/api/voiceMusicOnHold/index.js create mode 100644 server/api/voiceMusicOnHold/voiceMusicOnHold.attributes.js create mode 100644 server/api/voiceMusicOnHold/voiceMusicOnHold.controller.js create mode 100644 server/api/voiceMusicOnHold/voiceMusicOnHold.model.js create mode 100644 server/api/voiceMusicOnHold/voiceMusicOnHold.rpc.js create mode 100644 server/api/voiceQueue/index.js create mode 100644 server/api/voiceQueue/voiceQueue.attributes.js create mode 100644 server/api/voiceQueue/voiceQueue.controller.js create mode 100644 server/api/voiceQueue/voiceQueue.model.js create mode 100644 server/api/voiceQueue/voiceQueue.rpc.js create mode 100644 server/api/voiceQueueReport/index.js create mode 100644 server/api/voiceQueueReport/voiceQueueReport.attributes.js create mode 100644 server/api/voiceQueueReport/voiceQueueReport.controller.js create mode 100644 server/api/voiceQueueReport/voiceQueueReport.model.js create mode 100644 server/api/voiceQueueReport/voiceQueueReport.rpc.js create mode 100644 server/api/voiceQueueReportHistory/index.js create mode 100644 server/api/voiceQueueReportHistory/voiceQueueReportHistory.attributes.js create mode 100644 server/api/voiceQueueReportHistory/voiceQueueReportHistory.controller.js create mode 100644 server/api/voiceQueueReportHistory/voiceQueueReportHistory.model.js create mode 100644 server/api/voiceQueueReportHistory/voiceQueueReportHistory.rpc.js create mode 100644 server/api/voiceQueuesLog/index.js create mode 100644 server/api/voiceQueuesLog/voiceQueuesLog.attributes.js create mode 100644 server/api/voiceQueuesLog/voiceQueuesLog.controller.js create mode 100644 server/api/voiceQueuesLog/voiceQueuesLog.model.js create mode 100644 server/api/voiceQueuesLog/voiceQueuesLog.rpc.js create mode 100644 server/api/voiceRecording/index.js create mode 100644 server/api/voiceRecording/voiceRecording.attributes.js create mode 100644 server/api/voiceRecording/voiceRecording.controller.js create mode 100644 server/api/voiceRecording/voiceRecording.model.js create mode 100644 server/api/voiceRecording/voiceRecording.rpc.js create mode 100644 server/api/voiceTransferReport/index.js create mode 100644 server/api/voiceTransferReport/voiceTransferReport.attributes.js create mode 100644 server/api/voiceTransferReport/voiceTransferReport.controller.js create mode 100644 server/api/voiceTransferReport/voiceTransferReport.model.js create mode 100644 server/api/voiceTransferReport/voiceTransferReport.rpc.js create mode 100644 server/api/voiceTransferReportHistory/index.js create mode 100644 server/api/voiceTransferReportHistory/voiceTransferReportHistory.attributes.js create mode 100644 server/api/voiceTransferReportHistory/voiceTransferReportHistory.controller.js create mode 100644 server/api/voiceTransferReportHistory/voiceTransferReportHistory.model.js create mode 100644 server/api/voiceTransferReportHistory/voiceTransferReportHistory.rpc.js create mode 100644 server/app.js create mode 100644 server/components/auth/service.js create mode 100644 server/components/dashboards/run.js create mode 100644 server/components/export/csv.js create mode 100644 server/components/export/pdf.js create mode 100644 server/components/export/xls.js create mode 100644 server/components/extensions/rewrite.js create mode 100644 server/components/integrations/configuration.js create mode 100644 server/components/license/service.js create mode 100644 server/components/parsers/qs.js create mode 100644 server/config/environment/development.js create mode 100644 server/config/environment/index.js create mode 100644 server/config/environment/shared.js create mode 100644 server/config/express.js create mode 100644 server/config/license/hardware.js create mode 100644 server/config/license/index.js create mode 100644 server/config/local.env.js create mode 100644 server/config/logger.js create mode 100644 server/config/pm2/config.js create mode 100644 server/config/pm2/index.js create mode 100644 server/config/seed.js create mode 100644 server/config/seedContact.js create mode 100644 server/config/seedUsers.js create mode 100644 server/config/socketio.js create mode 100644 server/config/utils.js create mode 100644 server/errors/index.js create mode 100644 server/mysqldb/index.js create mode 100644 server/routes.js create mode 100644 server/rpc/index.js create mode 100644 server/services/agi.backup/index.js create mode 100644 server/services/agi.backup/lib/channel.js create mode 100644 server/services/agi.backup/lib/connection.js create mode 100644 server/services/agi.backup/lib/reply.js create mode 100644 server/services/agi.backup/lib/rpc/index.js create mode 100644 server/services/agi.backup/lib/scripts/index.js create mode 100644 server/services/agi.backup/lib/server.js create mode 100644 server/services/agi.backup/lib/square.js create mode 100644 server/services/agi.backup/lib/vertices.js create mode 100644 server/services/agi/channel.js create mode 100644 server/services/agi/error.js create mode 100644 server/services/agi/graph/index.js create mode 100644 server/services/agi/index.js create mode 100644 server/services/agi/reply.js create mode 100644 server/services/agi/rpc/index.js create mode 100644 server/services/agi/scripts/index.js create mode 100644 server/services/agi/server.js create mode 100644 server/services/agi/vertices.js create mode 100644 server/services/ami/acw.old.js create mode 100644 server/services/ami/acw/index.js create mode 100644 server/services/ami/ami.js create mode 100644 server/services/ami/client.js create mode 100644 server/services/ami/dialer/action.js create mode 100644 server/services/ami/dialer/agentComplete.js create mode 100644 server/services/ami/dialer/final.js create mode 100644 server/services/ami/dialer/hangup.js create mode 100644 server/services/ami/dialer/history.js create mode 100644 server/services/ami/dialer/index.js create mode 100644 server/services/ami/dialer/queueCallerAbandon.js create mode 100644 server/services/ami/index.js create mode 100644 server/services/ami/model/agent.js create mode 100644 server/services/ami/model/campaign.js create mode 100644 server/services/ami/model/channel.js create mode 100644 server/services/ami/model/freshdeskAccount.js create mode 100644 server/services/ami/model/queueReport.js create mode 100644 server/services/ami/model/salesforceAccount.js create mode 100644 server/services/ami/model/sugarcrmAccount.js create mode 100644 server/services/ami/model/telephone.js create mode 100644 server/services/ami/model/template.js create mode 100644 server/services/ami/model/trunk.js create mode 100644 server/services/ami/model/voiceQueue.js create mode 100644 server/services/ami/model/voiceTrigger.js create mode 100644 server/services/ami/model/zendeskAccount.js create mode 100644 server/services/ami/properties.js create mode 100644 server/services/ami/realtime/index.js create mode 100644 server/services/ami/report.old.js create mode 100644 server/services/ami/report/index.js create mode 100644 server/services/ami/report/userReport.js create mode 100644 server/services/ami/report/voiceAgentReport.js create mode 100644 server/services/ami/report/voiceCallReport.js create mode 100644 server/services/ami/report/voiceDialReport.js create mode 100644 server/services/ami/report/voiceQueueReport.js create mode 100644 server/services/ami/rpc/campaign.js create mode 100644 server/services/ami/rpc/cmHopper.js create mode 100644 server/services/ami/rpc/cmHopperBlack.js create mode 100644 server/services/ami/rpc/cmHopperFinal.js create mode 100644 server/services/ami/rpc/cmHopperHistory.js create mode 100644 server/services/ami/rpc/integrations/freshdesk.js create mode 100644 server/services/ami/rpc/integrations/salesforce.js create mode 100644 server/services/ami/rpc/integrations/sugarcrm.js create mode 100644 server/services/ami/rpc/integrations/zendesk.js create mode 100644 server/services/ami/rpc/memberReport.js create mode 100644 server/services/ami/rpc/template.js create mode 100644 server/services/ami/rpc/trigger.js create mode 100644 server/services/ami/rpc/trunk.js create mode 100644 server/services/ami/rpc/user.js create mode 100644 server/services/ami/rpc/voiceAgentReport.js create mode 100644 server/services/ami/rpc/voiceCallReport.js create mode 100644 server/services/ami/rpc/voiceDialReport.js create mode 100644 server/services/ami/rpc/voiceQueue.js create mode 100644 server/services/ami/rpc/voiceQueueReport.js create mode 100644 server/services/ami/rpc/voiceQueueRt.js create mode 100644 server/services/ami/rpc/voiceRecording.js create mode 100644 server/services/ami/rpc/voiceTransferReport.js create mode 100644 server/services/ami/server/index.js create mode 100644 server/services/ami/trigger/index.js create mode 100644 server/services/ami/trigger/integration/freshdesk.js create mode 100644 server/services/ami/trigger/integration/index.js create mode 100644 server/services/ami/trigger/integration/salesforce.js create mode 100644 server/services/ami/trigger/integration/sugarcrm.js create mode 100644 server/services/ami/trigger/integration/zendesk.js create mode 100644 server/services/ami/trigger/urlforward/index.js create mode 100644 server/services/email/index.js create mode 100644 server/services/email/lib/email.js create mode 100644 server/services/email/lib/imap.js create mode 100644 server/services/email/lib/smtp.js create mode 100644 server/services/express/index.js diff --git a/public/assets/angular-material-assets/icons/avatar-icons.svg b/public/assets/angular-material-assets/icons/avatar-icons.svg new file mode 100644 index 0000000..20bb670 --- /dev/null +++ b/public/assets/angular-material-assets/icons/avatar-icons.svg @@ -0,0 +1,240 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/angular-material-assets/img/100-0.jpeg b/public/assets/angular-material-assets/img/100-0.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..dfac4ab8c5608f6bbcd2dfd2f4d037bab326acda GIT binary patch literal 3054 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 X`8HGq7=bhk${5$>QtM|Svfn literal 0 HcmV?d00001 diff --git a/public/assets/angular-material-assets/img/100-1.jpeg b/public/assets/angular-material-assets/img/100-1.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..8c12d97cedc5e359bf21a7738614336e0916f943 GIT binary patch literal 2439 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 z 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
`uhV%C=003gP6;B01M^3mf6t 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 literal 0 HcmV?d00001 diff --git a/public/assets/angular-material-assets/img/testimonials/logo-bradgreen@2x.fw.png b/public/assets/angular-material-assets/img/testimonials/logo-bradgreen@2x.fw.png new file mode 100644 index 0000000000000000000000000000000000000000..b36da3fd985a6a600feb0d8769754bdca2910363 GIT binary patch literal 32092 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

%u|*+Du(OlDaa}3jRdXCT%S~H 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 literal 0 HcmV?d00001 diff --git a/public/assets/angular-material-assets/img/testimonials/quote.png b/public/assets/angular-material-assets/img/testimonials/quote.png new file mode 100644 index 0000000000000000000000000000000000000000..a9f2a7370a79aef1659d8527dedfd63ddaeadf5a GIT binary patch literal 611 zcmV-p0-XJcP)+#QW#~ z;qgd3YL`Jpcsvr;E3^6F0^qU8-Hg3u=E()XBaySt-ZS}-v6ROn$BliYzw>YKK;*c! z&x~Djk4NHRYhTh_U{6Q%m9(#^PPqpo7p;wO#61+b?`?#7<%xSB@;oEPu0C=PMB=iI z(5ydW4@LGXOKEok_ekWrv6Qic$UPDXGi)sNWaPk7+P&z`JrH@Cu!L$Oq!RaZWVf~i z{hiPri#$$P!uW#=v9wrSU7Vac_4CAnWj5&?hQna z?L}4t_wtcVdzslM>gkAh#00Z}dilt_6p-ZR%wrOE$n9^)iuF8=$9NCbDJ&)YQvFc5DdhWg_QnhS1XyzpvR| zLr+J-1iLlR(-B{X_aAyXatWVg=;?^}aI2xGBYT!(k9zq?=-77xdOG4K@QVjM9q|== zi52b*MZ%0}rUUgvB;{$A{ZBzpM?9?RKSS!Nh~T7S|G3hl5f6L4eWu?F?%dN6$<@|e x-&oVnZCyrfXpcv}l@ckD5-E`qDUlKZyaq)6tR-)n=D+{|002ovPDHLkV1mJzCqDoH literal 0 HcmV?d00001 diff --git a/public/assets/angular-material-assets/img/testimonials/testimonial-hampton@2x.png b/public/assets/angular-material-assets/img/testimonials/testimonial-hampton@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..65abe71c6daa7b90598a92b3a24520842525e48f GIT binary patch literal 12205 zcmV;eFH+EnP)`}X|v?epyA=j-e9 z-L<;gw6xW>x4gT&%(%1Fv$WN+uFA8m(XX%EtFzUrs?Dsdx3aOut*Xwdq{XYGwXLtr zrm3u~vB;#I&!VHesG`H9qtu(G$(^3Lp_|8l2lAuU~zU?QifDVW?EL0O;UPPNs>%Rh)z&_P(5l>NmW&6 zh)O|uN+~8x$WP92_4f z9~>MT8zCbY6)zPI8W5gHH-APfr-5DW|u5;O!177Pgt z3=$9w4GRkk%VCzwT$9gbn$K~f(|4ugrp4QgvgfwT-k7@AfvolQ_5b(#?#bKr+vx4> z?aynS`}6kk(ct>+^XtCUx z!^6WOBP#p*`;d)~OHE8KEHEMS~{_~%% z-J-X~SRcbQqHLNeRw_d3FG!(@9m?hGEQR_Kin<16GDUr|CHdG{rJ7M7izgqWa;Rc- zE_#!G6aN*Z(N+&}$-#&2G3h)-QdJg|MfX@&)Gsq$F%48!0BV#%v+0BG;JOduQ&rDE zeJLexCHoi#cg9CpDktEski=onOLa3o#_z&?5yDMOc}af;ZV2BYKl45ZF-K>>GHv)& z7voDmA8lNfRRgSb0lrLg2sk2A#i0b@CiUeo;fU_>s^u4U$=7r@{uNasfc}Kcw0qwU zheS~hX8>z%;l`!`Pq)8<1pMOKBO@0e{0=M*M~qQ)9|Y=ao!56XRHbchKHB0N{Gwlx zkNW;$!Ll6X-IPzhsQaA4k0(OUc*4DwpM#|;=}-W70DM}+tE1uOr?{;%LgoHsXQ%G( z0h^xW*9XDG(6()G-g4L7X1t>5x(1h@teFQ)8`&0ySK40E{E7q5N+)bVkk_~O-c+_X zlI^|s-g{%&UJz{G^@&4vEg;)Nt^mUsTEX zczV}OG#S!x_V_j3d_e7{CqChQobzBH=r{4eFBko0@J}~fR6ANur~;+h?jezl#*@U_ zABhj{MvIDT)@%ILCWzyrCyG9QA2$ug%phl`S=85<4A7?E^qD50oll(Sz*_(lr;{Y{ zZeuQ;NTpKQonFUCEE2I=FPo$x@@oM*w!EqKxg@n%3MzUqXzC?kGDs~0915F3pP%Ew z`P8P`8IX7_NsO8BgB5{IEbokKlqpa;2O`nZXLEAY$D@Vg9+ zO-RrDeq}OaZ8adCCIyDm+eA)m%WL|CWcpX|aMg-0{;S9y&J6BWeSHG!W(pL=53}1W zS1`zncyy~OUIOY(5NGN9-k;dh$QXzuBdM*OR?!uw=L+yIJU!=cD+U7w=Mrh!-t`3! z1Py1<7ldU$_N*}t4t0Z;*w9%vI8^R7eUorS0=#?Lh7&LW#Q_!RT4~O>77qjcms zlF5BOPaqJ4iX@?7ct~D&K`jOXOcE6xkf!OAM6v{v7)N<-0CCFFcx2ZrwtmWq{xCt3 zt^{1(MicQ6n(mj>j1}#DK3`uD#ezZAkPV}=Q(U@wrCbVAyVHQz;r3UIlr%L81L8YFba0)Bz=j>I(#!Q=7hQ&okn`{URSytw}hXOVi5XcT{)2!+PO z*qUKwP>(?E5nLfhlJIphGMM^ly>=V9fa5ox;cO+L!MBC^`s@vLUL)5htPOa2RHQ%uqkBS;Nr! zJ)ZdSyc;(M;Z#JJ&+GQCMp?*is?PSW+dlsI{g2=O1dYf+XfRU|xngoEALre=A?l@- zx6L3%vPIG|aypvSDg)b^+zAK`PAqoen1;rqmj}bp%8^Qq4B#?W>mUEI?!5u;b-=GU zRd*S;r_9?4*pJ_O<(XIRz8Q7b7mOJOpbdjTWmYm|&V2ro$Ys&R`O1riD8!&@2&xVb zgtf}42~vU#ZD$!FLGU5#C5_{+W7`ZgHXhgT_dllcliY*+;xcf(t9vbVN!qVJe&xBl zAG-UN8!x~7tdmYU?x-`~5CkrHc#(h&9j5RBTvr&+iA*njU@PBchk+EuTOEnQukpS2 z-a8et#vqJ7^H*cKnflYG`Qq^VAwe(N)fvUuR5Viq1G|bs2^(>as`37s^dfNOTK zFTeD{^G`eW$Upt5{_w*a-v6&(clk+I!O6KIsR|QCmmvzGOCg1+veawIlr`3fCmrs$ zEF4Fa5h#G;``~Z25nQyvGKCqAynWPSPWQ)t=&t*JD|A)HSm=bjz$MzRP;D*mz5eWz z58rpkE!Q;y`1oUw1a5tOO~a?A&K1lH|I<%9osin2D;`l5Uv=rZWrcFD^=qLB0aAa# zw6x}sg%hME;g@G}REyD)2hoQd`Fw5OktvYGNdSKz@dWV(0_|>ZFOtFShO}*MEiLag zzy89rPd@taefM2=-K7_va~dpBUsqF8U$d~js4$)BaIxqV6v$oDRiuPV_+W9N>|jj` zk0e6m1t?7uE>^a1%-YiS!GmTp5+k)&)cUYd`-~a3s=Re582AC;%(X=YBf;bD>4Cmp zH>vv`XkUL8_2i?E-go)36jcfeLv^XFy`+Fw(sNse$PF<> z#9*4FixDaI23pW)^R)yfm&Q{FzOfN4oB(eeJ-2@SP+@3vy`6vu=?pSDy}q}hcMwzZ zYS`ENk5wHcFK}Ob_O;hudiKSq9)0-s#!Jt?5WMyEhXc5J-oklv56vkqWP*#SdP!%7 z1i4#F&J}bP9Z80)5GG*61Aq|%XniBUw*Ap0KVs@#tYAZz~HT`18_Bd{`L$eKvkE)3|;4* z9Q@#sCJ)$)ws@dq%f|`oD@uoK79oZmEuQSpQp1H)(NI6yEk%1@dxA#NxOi1TeXk8g4MA;g9rgtWE8M2OK<6r~r=*Qru0< zn9jo&af6Gd3VkaAf7Mc!x2cJcN#NU|Fo2Ig61+9@!AsC{|48wR@HJG~B?(U5l{>Rg z#)Bm*dpdcT1_pWB+JY%B@Yf+k$d2P|)L|n&>-%7P?@dqp#B}qbC>dP$tKj_?8V_6hqh1T3HlDK@x>GZI!F)#J0OkYr6rc zmB51lCCWquC+O60Sx~A`8xpueUeVQNo5m6OsZRvP4b|QIuioA=aLYSQm9lsNd~0Ll zB@_~*@Y1F8Q1h28Sy+9PXo#4|#hgnQ0PMW8K#{QR6k3@Ww5HRnBX3qNefHUBpR$~p zs(dX~zM>~Nft*pWRb{3Lw5OlGT9hQ23_+G4;E$TiwC{e{^x9Jp`1adxx*o*mpMU(3 zNB*_04lzN*H8u0*T%;65W2G}E6>~YknLCkkVcYdE1sV066|4P&Bq%ty6}1&;^LdBE zQDv*>OJ25TM%P!tA=5ORn0vZaSG+yF|DtuL2R--sJMT1o)YSALkl|o5#O=3&7{HXn ze{DGIFyJnokFfaNtj(Cp&YX}d>O!mdY1OuG$y&VO(6*b*H2$hm6kO}*VXLim*k@!W zCMHHJKCnce;eg@}+ccT>H$!KFQ{i4R1Z|V(xt4c`*y4ThHFVrP_uP9sdE$~w;N#=5 zOdJM@mr(D`IWV6Sip5r^Amjv5_+)+MVj@9Y<4M{Yhj&TP2|I8ARHmBP-KVgL-EPZF zV_^Gcro+HS#SpZit@^H|x%r)sD!|loAin)pc%l)+1YBRYlsr7Ydd|TIrk8!tDzrL_ zQftvw%w;Uw0yC8uh=;&i-VzH`)L=)pE(^k8 z-M%(f*WNB?EA}o{cmxYPjNsChLWSN$vO_B2NoRx}ORB2)G}RBZrm} zIii^W6XF_Jn0Nv%J__P=QtTERz=+sKAAb0e@7)rV28Y)x^!4=$kt6|P5_j)rex4E* zgz({wOUU_x2Vj6-fBE_6UxF&?xGC$p$Z4wdBNzfr56bc=NfH1ixK2oX9~i{xXxB() zdpaHgfu~JNOqpHoF8^!PWMy3Urrb?oB+wvyO?V*+)&ugpq1!~%2fDrgGAygArpv)d z&49jm!NaZJDSQ{WgaBughIsv&#q*zmgX3f4<6<6-iA{VyCE=FW4HN4KA@5sKC6L3Q zF}_TK`1h;4;emnByYJ%3&U(34T3}p|ytE{e`4eCA^)KITR*|^A(T4r9{9`huYPTMA zd&ZNhiXKZDddJ|s2H?@5E8WBtt_W|9;bL@`83+QrbZ&A|Vtjn80i2K!^YFtn!>AeP zirxJ1@Yq-X@%SvkdP@@JU}$9cPEg{Xcrf71KtRP*q|4e6aN!5=5@15y8YvzG7{n3a z13S_%gRVdxPEJmUjfI7$Pm76BgJnY7QV59lH7n_vI2^Ue z54J$##)SZS0afq~0T;C7Nt(6p3+=7V5n!S*BsOQ~W@n_YPfOdrYjf&~6)Rqv4UxZo z_|f07t&%gWlfeKRKgMNk=bNJ`yT_d`>4gTp3h zx->i-!j?h}-+u0i$7d}$%o*Fl5JHy03~pI_5Q!gW13Ihf-f_KG8-=*{B_0AWv9SS6 zUZ<@m3(Z-3x9{7xZR;DGQ`5F@el2y)(if9rUz)R1%-q>vJKo@jvj#$P-wkZAf?A)h zg@X7VE*QooX;>Q$4kJbck$-&hz_;{(r1QWHXucr>Hu8ceA`%5Y7MC9BJVsu79df;)XTJ{rND%}3ZDUzbprhP82Y z0J1(I`nOCz2akDGMbZPRS64Xz-wgm0;=91Eh%KxD-nweRvq=fb&oAGVW38$p2XV(5 zR{I#UmS!`defu6ZUYAtpf) zc-8OMC9+q+EDKI-jv??3QsE>~_vvs3&5NSIvM)~-_^_h51G=Ipf}pdVh8qx{d;$}L zc<@ShyQ{g;>2eX^Dv~A+zP2iDYff=iPEOVtv`0-O=aprziI187s73JWswQ0zg?dNu z8a6idpar<(mrysL%s@5IHGp`}WQ&Hvat==5P*piWxC~%S%aTdgO}8D-WS=9;_c zOzGPN)+PWG;>Mbi-Sd+c&)-98?}!(;BSCl=du>s~vL3Zf6?tBgWJ#0!0`C)Pd*02- z0T;o-s-SR`gJTt;a|rhR1b+A~Mz36OfvK4YOtMWU)G`2B(?Np7IyL=GTLOMaCX8!{)CggH*(91aIX zaV#f@%w_0{nd3X$cZROu#F20U*ji|6D2LlYjkz7n zT}K$a1ej>pj7MWtp}DHGslC-zZ#Ac{U%h$Xf!xB{+H*~CgooJp*zQd$zj!h(|L;MJ zV|LxEsS^T9gkNMTXlY#K)qp~an&?LlpbI=rPs$USHb@(GP&Po@j@xVu`Zbavz)hGj zCK&=Z5#HPaYt@CW)^ml~>FZOsZryyKtjt<#96FcB<7_+u;HQ5Q7svfQ$^^>rxT1OC z1wYJ7GmNN;y2$SD^!xpi!U}BIQha+7yuS`xY?MM#wz100@=6;OrY|F-AqhwFaReAk z6uh0xI#6J3?P_g0b>u*LdOCOu%Mi7VEy&Sr9%oC*?zQvcW=x&B=kG~Y!!9!rLK)U| zzbJ~;$Sa+^z%!z#kL#kyT1+O>o=J4^vqaWmV;wA4R35fb41haF0c_ABM(=tU0-FzH z71X!7&Ye19bi@bDr_0P}g=zuoav3Jx^z@9cfAyI#Nt1mt6iu~(b+?K3?#iKHfS9E)Se*(z-SW}(0kfC=p=X@oG?!M#M|Q|2S)Q$J|@ zL6!MPc6M&Ax$JZqrn_Ug1lUQU=EUy#alf1KtEtnmFTNxBc(fJBZa0Sg_9Oc@E=ie{ zVk)rH4By!)iv0O%(;IJ?W=0Qe5(LzBigGx@HcOahai}j34gwSb=Ex)pVgMJODlDw3 zI&DQ8UsiVFMA@;U85p0B*0;Ago#>ToPM$clmh4^ujKv^-zRoU4KQ=mu{3AzR!A$>$ zB)^uS{*?^6|;X5kDA6sE%L1y4uM)xjYmFanj%)l5dDs#BQh zFm%+Ot2v8lwB*=PY=G9TJKA&+;=XmPw$*^R0-LHf9L(q!V zFQ+U|S-pJ!o*Z(?N7Lt-^982!`=jsFkkSOf5e{>_gF>)0C3f`m_guYtWpD_se>qSq^tD9O{N{$^&`sIS;gcUKF=p}eN zkhp;Xjkoi3b@iE>H_u`@ouz%q=z!k%BeGjr!FDL40=sW2^h#Z*kIJWPS-bI}4XLBqrGOeGf1(b&>hQ&aL55%?!FX2h>b&n-J!Yc0%JoRE-^ znEb-*+0*i6z>>>zbUC8Rgsa1urcBfACC|OGF~8Vk$}hH~1z>oAVT7n_-A=)WrZQ~f zDAp&0DTe1|t-S}p9?#&_w)VCwXo$Pp+Yp8;h`#7^8c*Z%N0s<&M%>~ZM@jiGXYY6+ zHgN%2v&BA~Co|R6IL*Asi++)#Y0E|cAAa-u8*gRa$}|<{RWme?o`|KHjnM+<`RH;a zVxDFNRX~#>sA{OYqrb=F>>2Fuy42>nI(W4U`XV4P39=c)rugW=bt~bBXLe+i6@v5mzkM)IP-SqZPVfWydpboVMLCybJWhL zvl<#pFY6OwUQUoS*~@tcu6Y3L>FK`;BfHuzb+tobBM7mvd?lA(yVtJ#A}(otMy}QR zwzcp``i^Du=Y1CU+1y3b9x{>Up?U%}O>{H>I9xtvIb3x3Fd@#%8?#i?6JvJdqjH*x zT47pKwSewpsXzeDHL9r!>b9PKCyd+!;6_(lM>`?{>}qyJ+TyZfq?LR!eoZ?1p?Y*h z2hw+}TeiL4;7hhMQ+iY!IP59i_A!zfSm1ZAn9s4s_|L50V{;f8Sd zn`nV~tkVKe745a^clhOiSHq41q@GA_b2{6gFNn!ogcxm*wJbY*)2b&I?Ax1_Yc4gL zbB+7*7iK>8CtoGZNtm+Jr=r|r%255GQ2Cpbog#o1%a|=34jca!cET2>CTw;`d9=U+ zh8Pt-zzRtVhM+a?*R+u6=|Oq!!L!ZT7JxPMpLR%m?cR7Tp7 z)aU1X`=?JNFOE-u!kV7|tD-g_UsoCgf#)bH>}a4^YQlkx&mtUD18ZwwqX!OY=uRY2 zlY@{MYb8w^mo(wBha@&KNHn(@uz0(432$w#ruteWg^cu7NxQOE&q*_rTmICU8}@F^ z*-y8Ytc#DGIVBGgaIA_>UDqWIF9Akw7_)I4NI87JlHwe!W1`YAaXeaJEF=QD9uoCH zz}VDj@A@kHB)Ru8N(X?+B-df!MxeXeT}`!Cs};Zt<2P(ie&a%S_o-d`SI_y|LBz#F zyXT>qc<39&Au60YpekaYrb+664nfgYac)7OSjElSfXgbZLvfEq3#~T4*Q;xqEC+)@ zd&f0NW-9~Wl{Hxx1fQa- z-3IYJM_lT@3taFv7Hr!WB_<{&uh@PdyYOUf?b$K_e*xZKJvwtE>t#hv)yGxXSCYJ# z;q(T#>J_}GqkRGkXfV4e3Jq=az}GYc%uBPfr2Kn3XCBkmmG5!CCGZHXNMAO!gbdS8 z+jP3py{J^BZ(&e~TA+=n0g-W{pr8yLMzdI1U&xbH@=BSIC_$-RKscszsV(BM5xz|D zwQE#Nm)ZV)`~GHeVtQ5XLCt-i-*t#Oe`cHg%eAw#Nj`k_J->VIIlp6FB@&4Ye;+F= znr)5C$YY`@#DWYky+4P0yuAl{dOY>_msi~XSZzy-XIJ+(zwtG0egAvMy4y-tZ#bk| z5zE#Rvx*KYT}@B3E;^jYU-|7R+l~wm4Qqa4fp%Xl@I^dLXXv+8+%R$((wsHSgsoXI zCIaEp@0>mR{ddPe23UAyCDZ5aIpFj4c-++$YH0SI;9Rj_u%F`%GZ{ZZrB@P z4|iG_^sU)b8q4I6+S*bi&rhZ-{5|2smnt`K^>@0R36)V6p-oWiO_p8duh(@up-00p zwmc@m9FxI@2*t!iAkgpg9_T&b?QE&7y7T^;hWdI^;Su16@42UZeQ8hx0PiS@+OtQeB z3(3zWu=r%Zuh$2#`^B33A8TrDY;19Ncvyvhwz~R>Cmyb>xO1;*t0{T~8Fq1#kjYkV z#EugQ3-S|rhnNr#3=I!Fd9}cg2c<0VvX!CRn(?L`)38`N9xN-US>fXXEF%u)2}EJ* zKkD=KdV4(EYo2M?zGqLHr`^-xZr)m3-`=&WrKP^6ZZu(1B3ZjkM(FpT;Umr$?oJV8 zszjU+j}l?(T>h@BUPnoT8&61pSwS+=F{Ue)&L}xCU|ETnnoW#AD)9}yGeMnzo-Uw% z-vP0>p{b1y*PcC{dpcSSh`}qAXIR&U6mV+uz+raqj?3qjPAG@9TVUQt_j{JNU_x7K5E6D9%y@ej~(+J>pkr2 zU(q*qJzY`L+~(=1fAkBh7Fqf+%_&nwr1WTUB(y|hSaig7)w-C;SqbhINKY7+mQXkw z9Znbk>xOMBH72(@oCP>Efv{Lw1&{eaJ$4+Ox~xIA(nAxhct3aB+AWlir@l~L?An%# zuuz;dkrkPW2ymQaM23$X86G;a;~KW{DH5g;RVr<;9ij7M)G`zKcmf%UYbxXbBMQZY zClc7OT$k|NhdEKVgY6UC7!gAnHBA%@)W z;x!yAp?ksLm9-3Iuz$&$nzb2r2~v>=+Z1R9ognjK*qw8bLxOo~WMn)vPV;L)^7|vc zZ|*;^>$%O9m6cnz+*w{$TKuPfnVO5M6iv}88Mn;1lng0z!}0uZB){nz)`!ECwVc6X zn91~#+lY~qIAE@68gZ3SW@3!{o$$&n52^w&%&<AD zY+8Y{0=yDr(K(3m7M(||cAn#Per*4)j+dL8o9kf}O(l4=-HPzM{c=*wT`Q%5m8? zF;*&~TijJuRM5@PS%?d_JFXRzp+yB=mg_&9^zGljtG=V7z8c`_$EvCT-f+w6V%H`% zJW_LcZO$afDO^rP$V6Nw$Jf@DvP_7T%BgrH1zj#4p(Q60k5TUlwUIKCh3iVbT3{=WqKi+M}U%@Je5K`tZ^FduSVleQU` zGPcbObI7l9?5>dzzInrzUl=<7zzD#F=Z<$*XypYiKS0dd_vGPkc6TzPsBh`?)E9_X zfLFPOetapJmpyWxn4B;t#bdgRpTEZb8ATLErx#3<`?8$N(M*-gM|7S_=?}-0$+t-a znJ9!pa8ao6gHs)dFU7&r-sWz8(d}{9)(LR|_!fY7$5RqvQ9MbSF;`>`Uu&b%1(nK# z^=vYpMM4~J=Iiye^##{kYycK3}&z=84KPL~mJ)X8UcVlx$M?*u+ zGi2gU`j^(00{n>Vm2>VAlkqm6!{kwIDw(84T$`R}Q!UFqWB3x7%%bK}8M*5%szxmq`$u027IVy##{#(AC}9=>Zvw+nXEeB*a?)Ucb8dpIjT~E3d@ckwK|X&z)VCjA zhq$$|+1=KR#f-Yj%8K=+0RK$;v9hHv1w)utM3NBmHFWKVYj-SUm=~>M<@#}9&^N&xGki(&rQC^ehj+83o*c4!15(iw&T4$M+c7{g&1JBn@~8!g-kHP z{Mo8M{io0NT^LT~KE4!zI3-`nWMq@;dbFDmBRF|0yu#s`$@gZ)gja~UM3-lpf*TqG z_gL?@+p0V#0q!9Y0K3~9i<_iO+*ufPWMbcIyZ5p96Q2_$bCitT*Liwb%OXr($U5ML z9))%${oXrgR)D!gW9Z$95DOK|;`@8OZFNve#)3vp{J!|s_X>RJJq^A?Bp zUEIqi$B}&eV{RgvnZMn2ecF-LnZ}Fe0OsW9%*^}@ZKI(x4za*qQ9z&r!h3j6Q_cMk z+}CpQje^Cv+q~TkZyUqzvHQ1huPZGsTJ@*5To@Q&34VEC_h-BJX+K^_B|mdrr*2*P z>A3z;j$@U%OX=w34DLFoRwvG&&(q2dV_-0F=1f1GKc2?whaZ08;ivoh`mnfLEN-T( zZQuUt_Bt7K6&2;B#e}%%qJN3af?#-Q$MU`xUWh}8TU$fjGgV}`d`$@!uiD{XV&gq%X&3$0 z?iW5ASae;#F3u5#pJZpiMFPv~gZD!KBVh&D5xD(qb9A;h*48}BxvwWSf3dG0U?FD2 zHPWlwCYg{(l&@V~TvSAF+|m-81$Okp()7ibzWT`x;ALN*7V30?7gT^nADowS1|r8{ z(RT;m=!3Vrqouy~xmti9+PtoPumIf2uxo0>WPt0coO|6ZSp27t1pPtQ1QW3n%f^M5 zUb-=#LG=8`nO|{t{sRGie`N5xLM#rivQdP zacdKqkPxq5Bg1av>CwFGg6YBK)bdM|;2WZylQZcVu7bVt63v{H!zTq5cD-KzJN)gE zzJ2sJ&pz~}JJ#Ld>+i$jZwhczLqh|^Sp3Wu9A<`DTvU80sA+5~>DqE?;4aq<(yf1= zUeWxD#rzl=yh2v3O+gbI2H6IBLM*9>?G(z9dH$t55a&Skmm_{@e<0V^b6J ziCP{z%E?5@f3121-e^o$_60wIxM5n17I@Pchc;cXIC`qDZx9vsZKWf-TaHG&jJ7i5 zIGa{pai4Fn&v)74rUul6#dUR$ZK>F>wyfmmMH@yf3xoCPLr=PHq+StV7#&`?dCM63 zNPk~nKQ2?yt^`67A;3>O@Fi(3ul5F<%Jxl%xK4-xrnE8tDJv-{F1}!eqe|)e4dq2U zob;|_!jN}EMUVS@f($R`o25cXf<=Gx?8EoLTUFh9iWSYi1C+LIQY^qiT+i*UVnbOe zmF-nlW^_3Waf9`FrBz+=g(X>TL1TTft8ndJv%#}oloobTfM!4j4)U>2B39{ zgq-fFs%`a-#G)f73o-8GdA~_`aTwtHc}^-T{rQ?VEnDs7G45s%i!u(eEMoy2vrhL4 zvg4uDV1VgwYIP4^tP`tQx>vPpLjqr{~qgF!BKnt-v-L(m>WTLvtDHNsu z^>}#a&EX}-I|}1YfHQ_-j%?_55Mm$gNC4xoyT#Ggt+lm{?cTs-%#P}@=tP0Ir>Ap| zhcZl%CBzhpwPj@`WsBtZX6cn^A;;pdu4ka8;geJxJnTKdNtrHB2fJG>GV16^IUJZ| z+f%l^Z4vvuoE!7TvW2B`o@k76<(n$z7Q$kL?!dLT4z z$$ygGo^)(E7;-`ZGWQPwCdDkgmacOB8noyq3JPHSGbj~?p(_b`DL+QE^)3nS&aLJmT9hcfB#CzIghFP`7_3PBFp9bOKi$G*Vyf1KYe}-dr zlfk8drO~i%=n19lpgo@*9kO#uWBTk^?De!YK7+l#X5GFsOktay%@_rbjf_l-<_$d@ z^z$UFC2ZVh&;|7mvVOm57*^D=m}P(-cV)5bH=s>BgW_*mJf6>_vssAAvqjsErcF;< zoZYf$UCe!?{G$qB#WR&;qWhU!T)!dR{;3~}A$IJ|r2(F`X*>>UwzO@Aslj18yI>D# zDx+JuNpZOI8N&OU)Bh|U(&1G~g2Bkkal$CVkxa{;owWdtX^Djo>?O^}Dl7MXpwGR9 zF#LwK>C@!`v5Y)6@GR+*GD)u$Q~V=&dOFUwrau38}=mGwbvAkm&pN{+6sQ=mY vgww0_~*&>yz$?>?cT)W-`n!u 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
%E`+`1O}wb#Q6rBoNgS3dQ;+gC0> zu(PrKcW?QJ>DF{_`QCck)7PDJDtevYL z+K7)$yWKB&H~N?nQNuHI1zRqKdEq{>b-z>uXH(^;WtXt=id%wMx(wTd4ygnZvTk zV`x)IWM~f9IZGy>Dk$Xu479Bv?_yr2PWoAH}VMb(PhNRLIy*rBZZM3mI*0lva zwa!H@dxl)M=5rtd4OUH4(IY@+voo8p(bADYLPG&aihBn3Y0Q4{%n(w_H8tk0ykyVH zu+xMH1fH`_k?q|xJhD32zh`jkHTwpgq6z83$qAp8#cY88IEv!`LLi@+{UdQ6;T+4O zYCglFEMiO_{qRE{`tbcHj<3&}X8(=P)83UU3+{WqpbOwc0MLM7-qF4Q&Q$;cP*X+3 ziM2Ja2O$4ah z>9x~((vYcZLsd`OrcSCUfh{a_9g>N7N7iC3ffn*B4h&QYOlCbbf@fuW<1UaG3C#WQS%?VAya8;U$%q`If+Yb26Hzw@)i_Qe5Rnn786jqwBkvqBh$w=Eh(W{N_OOyqidHg zGieH@F-n5GmzyaPTN|Z{Lcjpj$=vV$)w|eGEc(R-$U9PPh18<+F3XVt5I8F|Vyt3J zjVpXH=uEe26L1cjDi)3SUWcb0^$j?5$yFh02tMn$epjYuO;A9bA4nZSfMjIJ(6eQZ zm?(2ucS)*-Ad#9ekupdFsYmdZVuYU zXN1RQ7lD!h7ek}*|3dF(PL$fUVyjtFVdTNE7*Fay|Lu3*bNAzS-t|-+qe#8F_v(dH z%XQp7dXr|^6?J1k|7h>T-O=yv$pXn;sF)1^n4BRzizBD=+RM4(!3Q3G=R4kee*K)P z_+Eb56-VhzL;G8afA!Y)Oh?N7E@w2`s8o4*t>}8+MBN@& zig9JBFwazU4h>ukZD{2D$?+^T=r|<1dG% zBHn4ESOwDvXh@_0RwtaAO=djoKogxSEO!zWM9DexIrPzDf=R8XW;UG`gGI^~e)+fG zu{EvtFD(fOTpeyh$rmH_O% z0+^zSm?$`hjFMyyinb)2dmyqT5#}T*RMDa=G^Lah8KUz7N`x6Z_Abw}6s4(aF-7B; zT0~|h?{fo4DIsDTqqq5URNH38&LJa%0iiGfLaxTSiz3nnWl}L!V*`~WB2>lGkDgA+ z4qUOSnh?kۯl4tR>vq+i_5+ivGZgtgWWnP9+FCGyrl5J^QR1 zDh_SJw#stf0>vZ_IW>|Eku%>^wJG=_&w52WQUFLHu_ISFX%z%DiXj34d~Aek}~7EETL4dlqwKw}fp zkt?04fFTjVw6&RHJF_gn*>;b2A_~cFEAd>_XHs z;YA;q{oky(G6f_v1tsQA-Ze@e`{*No{fD3Y#N7`PO409N0>aKshi+Lb-P5D3(jj@T zb-;N)QSqXL4=)l!FG!s)p6V`^S{J^F-9(uXo8{%jH@@);if;DVHBWfYeBSAnj@VF{ zydh-XcYDQbQvc5H{l$1Eb-P7o!K$XBhzzLO#-@(8x>9tP3eU(1+ZftWJauX`ZY^h5 zFnLG4PL>zmcks|!FIDl82hIv|?nqP7 zdZZ}U&!{&nLK|k=P1bRxE4dA9*7dej;?si;j-o}yWV=`CLYIQ}$Yh1ZzoEZq3L1!v9({Ns=$5mQwH;ePzoqVHpDpn{h zigaCdj*Vco>tDQ_R_WG|K$GsHVh@(dH*ttCJv6=G|_djTw8V zqC|wIfEr^&Ge%_3;2^3Z00_q#P124=xf>e!4`gvhxpM^r=t&J4U5T8i4XAzHIN_L*BcywuLxYNwKD z@) zH@);RtEPA3(l~M&v1ih zA@$7cAHj4lknP}t+ytH}M~#?L(z9o`{_-zAcGvrlH)(2RQpFHstIegutFGR2n=AZN zr=NVow;p}!a*`1%f^PL~N^Yr>D54bsLAOaDa$E6j&G$@rAsPJ4{g6-`X>NZa5oyPi0 zy}=-FEMzGmJMw=2T7LfI_~etL7I}4VH}kl;(e_FnZ`3uqK}pp#HUOSrsdP%Txm^zy zdy}1NG@W(IA|onF?v~gqi}5(DPur_68FJ=fltd&W-Ar&#Ho!JuVi!qM5)w2(mstuF zA}2ymh!jCklu}Sfh6JFX$ciQaSz%1p_tXf8T2rF|nE;x(99hkKnZ+jAD?aD;@BPYG zUU|uCRW&i`{DS|-;*yIGc3LKBRggVJ%%0*1*v;;CS*S?Q>w%|b8xyl?oz z*5{_;yIoggVKN@ybI;BPKXmq;?^u8L2i8CNsjUxxjJ+G)e6!0v6*=bxvXdC*dm7?} z9R|_naVHm~!xu2T2>47KjgcPeRhx&_yjwArq zUPq=|^;3_HCbQUE9t;;&7ni$UXPf7@bMoEges8Hfy4Jbp(-E|oO9 z#lEWx1!o=e1r()rI%d%lsbilNMTQUFf9&HA9Y3=^y8P(ggKLYEssRHcdSJe28#DMTZonoeFtw!l}{5wB-*MEQKU5{+= zk+se>hx2B<5txXWIQon*%dKf-0(Api%GLY*{)O(_?jAWtP~6RvBO=GDI=@NJQwfw{ zo~369S>{qmHb2YF_m~uzodKjI(KPSm3u{B)$+S%>F@^+8N|H4QnP5^AO)8GP z@|-nEz;ksBvg5*KK)^`k=I;lBd8DIhNJ2zqHrp6=`<-q#6I2vXGgd-In;%XQ(Sb8j zU@XcUol^s2&yE1l@{ZHmQmdLk5J9~6Hg7>`B1FnEUK-TflUOy_$!CT_2!@4;cQ|V` z0&x^JAR|DeIg{TI89alM%r`}KY$m3W^Bjqk7!rU`%Cf=-QUfFqGk}aO0TLULGjixN zfS`)T4haoV4UIt!ESVX|g{E9IQ$kY-F%_Ls1Gi_+OrAQ{o;g2zr;9j>7QAD^{$L^jV z?P3DQGp3CTNML|xheFN|aq}2^oqHu=epv$A91&;+%$#Rg+q7?a`-jf%Yz_O}P&Xr+ zl*@}tL+>&o2(e*hGK$SKR3qJ44_NTxuvlIwVcIlpyR#js>~?xt-th(5(+{7YROymS z7AVH8TFSk{rEdQ4W1D$DL#Js9!-ei-)(!_*$5Y^J{ruL!qr=&b0AP`WLz$ldG$r%S z8#R_x5`t5cBF{x|XMJj&0#_ESX`pt9XthxenitqcBvy@)KpQ2A>KT(aPRSA_(mW$U z0|lwg5X-^}X3AWLB&u3sGpX4!2Itg_s95U2)`|5uZ+^0=<5z$F?Ux zVoXFl&w-RAsyf$|f*L!{`W??KZPFwnVyX_&5t-uRP@_2zf{2lrK}-x_E(jr2 zFeO5d4#0MKExcPbIq!vHl0@i`sF~Har%N%KV^mNR1aL_6!y`E2{a0L~NS4en%NG{S zgct#VT#mj#E)Wr7W66|AVk9s$Art3J(50*lM5U+8INh9uJ97Qpp1i1Z*W`9--|cn4 zL`ZXID=JV@sk5@mdw@)iQPGuIO37u}d_PBJjj@jLaF<@wX)lG?f)WBDkeL~JhP>xF zFUk3KQm2`eOC4rxCIP_8l1(iIBf8c8m<2N9Tdbn2^9!z&jjtsW-qNHvjIK!R3^qH8UWBvIhl0 z2+5ghREJ39dVR~#0VyCa^ifHs(^!SO?tA(jAG&X6)?Rk$-qnR37{vL_36Xa5i1eAL zI>dk4g?+J0aiLH9Pw^S;Lfruuv$|$`mivStDwcyQXw9V3zF6qA|}0IIQP@4dtXYV+;K=9!Kl zM2KqtW1#@RKtI1GFi)6sR%VE((bBYStHuDGbL5>RWVEr05y_GW3eS>t4WHjr ziBy@?NNu8+V-lDpK2zE0EIo?Zxu&g(E{Bl$j;5HT&3u6h>Rgs)-#e%eV>=aa&Vj0# z5)m0XGIlu-17QkkBrf+!1QGI_2|)qb5hdA4kfCc$Nr0R$isIbgytz7iemtI?ZnNz~ z0|il@h^j>Z(8PU~n)tD4({)LBk#KtwdUm@Q8LyWKsVzuLt{jR6u0p$(Rbdp@)ItH1n#zV^X~Cp2wS!p#QjJ`o`7=-uJ#wzyBi}(Z)$u_Vh~!ZZXr4#J10!Zez;wjHaX6_Lh-%gCYB_ zGawM0ZBK((e^`_UdV>xZ=uRBpI{s7z9DBq5!6g!rrGv|tuJpFgjUK$~tnU{#6U%9B zAwPX?5+m)~SCFQ&Cq`BlM-KH)pBf`shFH(qZqI20yUWQ|ep+bNp74!&dp9wg})2jN-zy9#M??3r1pMUFft~<16aUsNX zVZ?y|{7>et0sP~_)}EQ3&ENdb2Wg0gaB<(m3sD*{_gRA~xc`xv_qXr=NEO4Nvm6-q zEx?N}@p;E-6?khWWj*QSS(r-OSRibqo+ZdBly=#XqUT5u&{SIiBnCi4mpL`9tHxA- zabEvPREY^3=IBHvl4ZFo7IQ+H4;ey~%&}w4vcdpD)o4mZnY+yKuJ%=&<9W5gL{4{~ z;Rqy>)RY_nl8O+4b7V$|%&}8|Ce$P#s!UG7e2$aNYW>NLJy)zijGTFh4w4!n05gNA zIRFKPyjSGCe0u!c;M%JQ!9sL}f*G(!uL+sOC{4gXQWJ>|_4?24(UsWrXH9)>Q!#Fv zn?*>50V9f4(QlY}hm?+YY1n$BK7 zz$;Lv2@U4emjUO{2ac^P7#yc0kZMAXk z{8oYl`CWBmKzo23fFiQ-$tZxb1gUCm%j~EFPG$ft!6_{#pLqg4_TVM&`}B3!UD=*H zU!6EL33WT{NYenYneEO<=U=PZ98znhL=K7Zf4l`yoJ)>ULa_I~^Z1*7_tPDvX%<0% zi9_2um*q@NYSFnvI7Oy(m+c-A~O)5&iy;UPX+0ARKiX`XGSJLVx+B5BRv(-!b(gq-}-Hg4nrp8$vG7q4(F-AmUZ%slV%{oSj z>PkH#A*5F8hS*`A@nAq;su9Sk8Vw3hoW>J9w}ZVtb<5bagwAJqQTTHkn?L-se|GaV zM}Oldzt$s(BJ$k4j%c6=bm2^m&rXJiU2_gz#7zMHuVzESg+n}|neNJ<5D~#KzUf^b z`1He%mAxDwA?Lju*R&nqJ58H440^V;BiqTX*HM-&opr>|s0e z^w#O>WJ8VfK1Yr*84;lf2}m?iae=^RMl3s(+i75_F8~M`?~HM&g(I1Er*aEzy&3zT z&dXup97#w;$#{ngRhwGG9LzL<_X_6o3_5}+ni|KY)Qv-H88t@a3^f4&Ffu??7$+)0 z1YwzQK=ItV)@WPFwFw&yr=wh5xRSsdt}84O>_c}v1|;{qUcT9Cm!Z$L742N*JnMCX zewP<}O?&nupXx2-eo$6h$N{Ws0c{>WX9OzAVoNT|y9+KayTg4&XXvwnxd0%;3z1~) zteS@5u}7bOd%WkPpPZfFE(XKeRausaMgZ`g!JcffOXXB2<7$-U3%A~Wq_^ND>AbA{ zpHY0DJJSpy(}oU#N1qa@*^^vOgH~2~rGAjIATm zc55=;G^t6$Lk}JM;DKE7y!HPIar%x3EpZxU0@B8?p-~8n-K7DdCi4hZ^+C?Id;j=XF zW^h6Heo+&!n-6Pf_)oh#E=UAXZFlQLq}@}zDij%?9gW}m;ZMw}dayWDhvdN~y7{SD zv#?;-Usmj0F19!8yPw*)bHgR%E_IeTKM3fk+O{H5hhf^rj?0V)R24vDL`3usR8vZp z#3K&+`M%X*f1!W^PdqW&nTD`4ON3_Roy$Bi86bur8qIkFKx0&}Zkc<>fDn@Sc_o^X z%RB+Kvx>bdd!?v?nI$z5MW8%`CMH#4Cn1dLhLH9wFD@+f);C9I&TfpGpdv+0&@ocKPxZ$-355n8&eR!^UtMoGnDxp&_{+ooz$(Ji~3%m;n)@vRoJ)t=Tnm9RT>U}^oy zP4?)~H1{dg8{755uv=!C#1t)6(@}T0_=*?5?7+Ug`!BiW$fcKFcFE-ni_1Rq>@rEx zH0@~p> zz?F z9HV!nEgGuMJBBgM`_`5uije49zccLSMaTCRa^H2;q;1;Rh>#%|_<|TLwN3K#I5wQ0 zZxMkVGZPV9XuZ)~I>ep<%uIadoOg2;UTUM6viCs3K*E@@+nH5F{F3M0d|-9)vI7SO z{c>kCJ$`C^G?_km=Ip(X9NRiKT5EJpv}eb=`o9ZlC(bN!)=BNm5@ux)>n4Rj4*agF zc>cTc5~hw5vdhZiit7Byt(oLmQ)pd5ksuHWC;)>cPg(+Y*(QmqfL)TH*eS3MaBh@U z8V+@}lhIBM8?}^TG_avA4|9vQoiPnQn07#2nvDO0O0zv=@LNDt31B_Ml3mL9B$`JZp zlmt(VVOYvaMs>u+0S^oGs!fF2$Rd;-C^MZ-lM-i{*N`*>!=$k;`pZRcd1=oTox$3? znpA+;OzO!-2vrJ=Xmm&}qsQ)#ROEYBhDA5O@|G(<`|^d~@CEnZ{lLQyKJpK5dizH| zepfljkL3H3;Z#RnAmZ=^FTQMHudgP8W`_SX&`uA+@^Z+;q~JuC*?wM^r~gg-pn?qy0BPKnYV3t;-2j(q=jxVJwI~Y z?C{}*o#Q*BNuZoH_lGZAIe-6|nbE!j-NN(Z_nlf;T6)9Re(meN@T+dT`MC(YYc$&4 z+1?6GD<;lyk$2V(95{UWl>h**`62*)+56x9zBm8F-5Zbp`Th5F-CBvgSvP?yr39*! z<#Sw)Wr(w+qp6mK7M`|S6frR8a$Fo*8}RHIYwMr>$sc{e9nWo>mJ4rL28fKLXrur{ zY6?w6$Ci?UF|mnZN(e9s;XnQ7-}tNd-}f8;_N#~8Vy9{t8NuMfuLtcJ#pk@oYNE)z zdhptExazq8GbJDc{h!?7o-GMQn+r^kx7UyDoPCTqLqsO}*xe7PLD?zI8xteqI`}uA z*ROo!sr`qxrh~rZLt9!~evv=<@P>#Odx|9pCuyi-MNT+2b85gEqp7HZm?FZUQ|?>p zt}K;V?v+f$woZ&T&QErlVC)cS7c#1#3gk=;nAkG|0TE~3c~35Lc~KIWND485plO;r z+Kj1fqfZK;M!?=9k#|6ZW^QLZzUHz^zUd9Gx$Wx9nR&jQ5z(A-6H|QfiQ_-_-`{l4 zBTp3}X{ zJ!jeJv6W5&;LNhABtq+Ba>Q)dCFnXFx-<%Ir=>kz0DxgHDB)96ryxg5T`YLgPy+Kz zRq#ydb9Md2?B4A5JRFpIaev0=qdzeUmzBh&Q&QjUQ{8G*mDYJ8vlOw7(l<0zCV+&3 zph-Qd>yoL&M$QI(td5?2$&M@(=3dt?_mp^gYWC(<)o9kOmf4OW%JLfmjD?ay&zxVroD!(fv?-(4JrrfpD&MLdF|GKYS zSzd#wx7k|}1|E{0?%P)Mzji3L*>vkqHCcZef5Ysun;DW@C(1OeRdyd|Q_z7%l@GFa*&RR!fY}=&KCg&#SH)lH%#efB3#^g9P z(G{65O2-5QCQ1$stt|7h%zf@XJ5fz035<=wA_yXo#H64ubDVhv=;Y4Cw2hQ=t-?s( z`bDpO<11d)EsJqAGXZ$^%>w|*Zocx;-}=da{jLAzKc7Cae&EtIQ)9$rW@uu=e{T)OaMq!5WRU1KFwzFGTQEN@)=REafFPtfz&2s<_dLDtw+0Y zH0*GtBa_-~SF+SqW?Jo{i5{P_Mm*S)VTbC7K-NZAI3vMcv@eOL|1w*A%sS5>jEl&V z8EtJe%iTWbBs3oxC?-^kL^=lp+uV6d2@ui9l7Po1q9oi-c2;bdq2OM2>LT%W+fCD}j3Ci+2F+bPT%H3G_Lsl>c+=D`#orT}Jwv!<5OcBfoA zdi5*4>xjy1bbj;9BlXUiHr9xUK!lEn!Fktl8qx>;=H$ojtrobudE&$qlcxi=qfc-D z%m+SI{E~n9D_?r)bys}ZH^1>)zVquo=jE^XsUQ5g6Zbq3hdy%uwp;fdI8ua4iRSy! zMZm{DE&75BB^02k0C9J?XXW7agT?(|wt4!Y(fZRdwy_!k8aj_aNQ97qyO>9*gHG?< zsWX4|_CNdh$3B^w@S*p;^ADDO>-Oh8_l;lrt;iCVlul$Z%p7XNXZ@&Wo-t+E%`0GFa{l_1^b7Ot`#Oa-zZ#o#Zs*rGT;4@F{ zL~7X{rLb}AiOI?P|Nc+^c=?ijFMa-NuDRC7vBBO_vHtc z{9>Nk3Ngd&nPt1_2jYwqfX}R8nkU)!@)S&)=*k}V`jg3IX>swL?|RQq{?yO7vTNNA z54zwO0Wg{-<;-gc79+ALpd+51Ucc<C1_UplyF@7|S#4}a#7Pu=_Ypv+$NysOuD zrt`{`9rf~3G^LnQ+BI|=0K0sinX_^7-qFSha=Dq#btwPSydVXebHnoP;=wDPFE&@n zNvGTU#Aoh#;`F)I3G z#!kukcip__Cq8xl*j8157G=KB>ntu7xo0eW*7ugIS#M?=)2dCIRU5#72~35-d3HX- z60^bqkg6eR*~zmqM>iAM^O`Th$_(((5otNZQuWO zU-F9EZ{FUSj;1ptBHoquJga{VTjTNaVDSCl`sLsIOTRhYX%<$xf@n!G;oL^yCX>3W zC1sUaW{JQNGPJW9xdkw9W(sWN(1T{WfbM$U3X0Cbp#n}cPE`pkBWw%@=oBOeAY&CE zH6D8Y0tf1^Z8(^}yy7 zj=2U}3VL_`S2UR;#*$$!q2(-n&*E15B$<-%ii&}KU-Z}Y8Bf_Q$$%> z>&_y_B$IJGePWV%URdh3i5kT#F7Ind$DbHuhI{t(rd1s7>yOV(o_KQH8Th5u;g9{~ ze}2h}J|6&19Dm|xe)8Xa@I&u1YgLIMUtL}4^g0^AcG@OrH_K8I%F4dax#+B}c7`2! z^4!*Y{{ByY``3Q$ju+kX<=^oiUizxn{@H*0g+I9cFMsRr{_4pSr~Oh-Oj!*XIL{Nc z7!`AdBG;`k^m0^?>5Pc8eg~`II455a(Z>0Wk9_C@S#LFE9y}u$?OB9j*9m%dgq)^T zYFZF+nVZ(r$HMsh*48ywUcS1r63?9rZT!XGdgp=F#r>;;5XG@;>h`V&pI%<*tu7C; zdBclcGRun|5vs^|Brz{I1Tzfvt00pwJKf16lRNj1eKBut`%#jFnG3aK+LXpe} zF>TbFr$?h{TZ;x&;awww%M0>Mj*GI)a@H8kfHQL0|963bi3Ev6lzEQ`D1n?S%1rB! zOuLI6_Abk_wr*lbjxeQ2MC@GjxZ3T%;r2UjyY}+U?FoPq(*+~DJrk8mhJemxqq@5M z@PRw7z3Sm}r-R6RlCHZgN}%azCt)Ual-QcZUEGzKgk+eD7)VM@1JMjmbSvVCig#KpFclm`sEyP? za~>(^p2?^RSO;WJS&m`awDl}$0*9n1Dr%<0^W+}|1Wl+!3NczzbHoNTO=u(~Q42UM zuu}jdl3_`<+{0!%F;EHN^j7M0Y;nmc5ylXzsDYSW<}lCDdL7A10uRDwmKkvAkDc1O z<;%YEQr!X zMf!;4N_KF6anSdQa&2XBWa+W{j{Ww3_>*Z>4VH&5e8Wrcc*P61t9swTPD~L2R1uIb zDDv%}P<)|o5wTXVa^G87i~pms;Gc~5~aw?+~;Rc zoc!Cr``c4z&oFj-!*Xe_WkqV6x*eyE@1B0*&PPA<57W_x_x^jo@B7z=-Ho%Swzg&w za;LVnz9}ik#OqJ)1WCQVkLPwYG%JV7?&8AcdOh9=S&<{<)GNDPw{fl=3>RnZ<||+E z`SUH$0Dk*7e(n!`?@!ha?Af!tNa&H&JBz7p6|~IAWhDYJlznIRzzMwvNzKlh(I`l0KoQhxq%5zh&n2Zp-_jy5L z5R$3suvAU+MAt0OvhLEtp4Fw*g(bs!0}J|M(m4_?r(sbaouF z%rg+Ex~yY{q9P`o70elP&hDRh;f&qp&YHGx)mQ2B! zuq!N`_Wn$A!)cHfASPrpM1`an002YSp0$U1^aYpo_b!$m&1Zy61jdb2>$CAJ)M_=G zIRcvRsSHHKSQL527$-AF*y)zVpkzlXNi;GXd>6D-v zrY#YgN}gq=0Bp&0ZE4|^cifgDFc>hK5+d(0J0)C4>=PDW0pHB*3Y4)f7kk%1|4PGz1$ z3Tldmgvg8_rh-|A5X@8oVKxQlFef`+>ALAY2T82sX;@$a?PlbdnteJsGW_S zFVH{$0Br8P6VsxT#YX=AFYh}s9Fw9FS^FeK6g zZg6Vr^r^?5y8Y$PbKWiQU(WLCio-<6fIw)^c$EGLY#RdZ60+EyY#uvmm=xh zQ=gt}oo1gK*hRe`g6&Ek02H9p>+Ec8{oe2Y?z`Um{*7}xWpB85xijc?4z2B9?5-U- zc=-!o@chsFyjMT>*5{39;rIUN53alR<~{q4bce&8(e`L`w%5xvbG;(-5ze2RB0^dC zB*05~f2q4T>};Lc*jTUDu2|rt)ui2bsIz{m+Md*#+oxapvd{fj-}fH|{o!A~`Okj+ zH-6%iAOD2+n0sj2N+0!HI`1(|E2#7>RgmVi$#7G&!@!EFCcL!_$P}_7|pWmKNViE$Kr)dI# zcUh77oPjXU9XZI#oS2bp*Oug*E1b_9ikZm&_$WbwrfS+YW!<7Eb5)QO!>pF1K0}y4 zN10qsaT?zE;+I@^_(+?A!R|cxg4N~X>wN8|7AnBOwdKc8o<6_5nU$F)#h~mRd2&WF z-9BFq2i<;0tBIg%(D`0pnklSrWc^NL&^FAXt>pgF*gm}-20iW*S}w}47HKuZN5?P~ z>7X-!D!ClefDLFqSVwX|4tk~{*i=w~_HsPhg=xgoQBT)yPnHH85Zy#5Gwz|y6t=23 z$Q=WlH=k3p6*F!?ln`{6aboO%a|9C&h+rZZ5-gY42AQZXFb+MRQrlmEb3{O$lGfLi zJUfSG*i7P?EjYAbr9qmcCPCF`zhusV8PO9BhsZ8$ZjEMd7zLrvL5N-!p24%bg?Jcb(g;k8jx7VZM-Kwv$cH zZ#+?jI&-~z?6^RjS`0W{CcWb#*k_14dP<};5!^~lcn^w0g=pMJ@gf6L2W@%jJe zhri;!ho0>A2RmnGTN_g`SX#NtT+1b%kXRD<>L)~f4pP63%yhAu^H&1T& z2A$9Qys!MBAO5-3J$rxa*MIRp{?dQgyVf~%S_i#C+X&7Pm_xx{-s!pVRLcbpCYg6K zl`4xXxo?_exnEr1N1upIh*$3OC%3id=mT&0>$@L0^Q*u33(vjj#y|eKUwrwufAz6P z*Lzu!P-Tt7FuLe@$}|;7jdpYL9+gpmGoN}u&h5&;jW{cPuiu0jgbF$ts(N!4)aMx! z12G{+RAlRRhhwbod;H1g-f-P5x8M4uDO#W>rYUY^Df|o_vwzWrhks;5}z$<}%J0RYc5)*kuHa8WJO7$7SB9 zDvD}b$0X7n6lJ$i(Ii1b1cJPqff;!+g|eGXwnsN!dHJiJbIY`@FATKpS)~5?tL;KJ zRBi5I({#zfJR*#QR0ORN^eY*We{pRJ;eh{s!iim*LSSQV3=EMsSegQPR)<(`j&A$z9~&ILY~!&9?)%MOux>i^v(j8!Z<#roOPzy7;_{_vBJ zEiV)Yk6ixgPk;Ike*fpc`(OR+%Idx^|LX6(^ZUQ;?86DrRt}b7mV(N@mAouCC75l^ zPMrw*4t1rew|DgLm3w=Pk8QL8Cg1SY-}SxU_a6Y@z3+YNPyFn^KYH2T4rHM{6-pPG zonh6s+v9fMYIin?u?<})0%Daw8ADR?o`z+VD)Wn=u4?s$OPAI^b8gbW^PYG4TR(i) z=YGzs|J!f=#utC#7yYMy_PwwFzHjs8p{ypq(1f^wCEV@rpwS%R>jTTiWKP zfOeds%ZSK4OwXTfQ!}{qlGR~j>OD&fQG+W8)dY=-TwTlG%4$baQsTc5eN{`{*~ z_hsq;TeZ4XRaAqJEFv$n$i`%I942);jjf5!JBplpUwG%8^DYK)p1Q-r5jvu#Ze2I0 z%o7q5g|;E3qVU9IYN?9Nv;~5EQ1+Z>2dWyXwwhHw&wMANlq8xZ^*t|bH0QtMWiNIF zjVj}X+rB+Z^ax;hA;pdm!BmZiswQ4>_+aVs)I`i3)nWpw=7^YqqNSZPC&P51v((KP zG|r;rSQKp7%vzT@OR3jPp{_;{dYVlFD;TH>xM^Zd*72^FK~!T%xrfxc3AnC;Lp19I|e3zUG|s%?zZLYMjkF2fjPZUPrmG-6{j zMY5E@J2ZnNR2Ep*sS)dYK!S0Dmn=}$VI%<6liN_Ml@2Y&r`K`R@Sa{`w5qYX(cGag z7znb0jD4t+)ph2*KiVY|6|A?Y zB`@@Qi`zTrS6^`TP0u+zJ@@Iw#h0udy0IFa4b_yKo6D2`>G0``SBI6uJu3&UQHkfD zzS}}@S=TJ?3YXE~Lal7OQx_rwFx_|XAfdHQEoKZC_21wGG2=7m&i(H1{N591PxiaL z6p=gabypwy;QRmT=38I<+~>XG#V>r#RhQrXiI07F@7_T@OP=%nd-Ed4*zoyNTTR_A zuMJwUHo(yv7G1(e@83#XKmSFq{HAaIZWZ~+$3FD$e&jp%9azel)}Puqa$u;A2n>Qw ztH_z_c3lkWN|#v{LP#-bMrF6d5(|UbSnRN0Tkf1aHw!ne4AzF@r#IS3b?N16pZUbo z|N48s@3KoTea-V<{2$=*e<3~cbT#HPoTIBkqp41BEW-BpZKm{{4HYK zdvHY^_~ZZ(JrWtPi%Bta7mPb^{Z?YP5zpCLE`AR>gm zS(sF($F-(prj8kX?hBul^Mq_O)$T%3W)2BJOqsHU9wJDLNCuXQA|obA5~HLll0)Cg zeNp%vHAZPIN=j{tb@bkmf+UKX@{FjJF~nEhe%p;lj?SvUOuJC>*;aTTB1F&Z# z40Y2S+OvAqp+gUzdMeL)Mvjwbo10u+bXkc?+Z$(_QAN(738G3HECeNNr?rW!qV^-6 ziE3ZGHWpIQ z2oPV%JC&Ll3#L|LtrxRdQ~?wn0irXT6QvWHwm~|m9Y@J9m~=brx;Zd} zpreM6AR_>wlQl)ufisC(wU#4#r$oq%%m8g;Rh#wmeE%YMOUw$_=}XdsFL*gRck&Y- z+J5Ms-og;OT`-x;gn)=Z)y(H~&x7ZmK1G9tLpz)6n;XaK@ivSx+p611y^})FP$DkV zQn|Fxw347iVN{LtY{_CgHJjb~vP-`Bl}GEH6SL{M&&uv_uV~U|Vqy@VWlp~+(ck@D zs}W~QYnQpKXzS^8>m>V(HO}?Qv>Qc*VDm<>8q9-*Q9#;iQcb238cj(Rn2DK4QN+64 z^6h{B-bar=e(B+r(X2T;X>yf}%DRs4`G-F}b^I}(m9KdDE0-3j-z%u>FRyhmSN443 zsq;^q7-{DBER>To)yitt>1Eqn(*zKz>SeF|qT%8qG5`MW{mxTQA3t<(HPm5eD&`pr z7ty9IC^^mu4B8eYB%d=YS|ETN%*6TB#-i|pZgNgG$FU(?SuUovY;CuTJ=a;?|I|}Y zzV$6{VaH$mve(=CG{!dYSU_}CXEUF}zzkrJyWBx&uoezR-2%IX&95#ZFyNe^WF2&* z-^Ff^C~qb^L8sbtY^eWZ`v@DOG#-P8O=+X*!qQ;r=%L=;edTb8%d%Wq^IVQcGl+8Q z%~yWQ*L>c0eeP|~>lH6P(7W}r6~uzsA!v#X0wL{M3+JWWynzR17ZEG-*C+|px}Dqr zwkH#j)GIRrL`KV49YRK&dm>5#1`ZT=X@Z0%QcY%(U?JzNs(JS#Tfh9^GbwcNzopmM zGXO5z^=T!MGRqGvFSj8%LqJQhO<>N9P(4T#Oj%yeW;JZ_;>v=8X);k|O_uqpZjN-q zb&H_e*)##75tx8C#Ga)=PQ5}rK@##N#z2f6(IhAa2!;;vB2+GF=3%Gy+NJ?RO44PV zMpvsV2n^MW2!aP=MIxMtn*GAh;^fENF9&fn}PxoKx)R_)wMm(doco> z_~b{}7h+ChLge|(91YYc&uBDi9)AS29Zb)kZ>HxD9A3EYs%!QR7IT4}Dt+SNyWp5D zxWU5FBf;0(XU_IB+8Xcl{K8UJo;!E;@|*X)`ucv}j;2P<=r!Z z4FbZ&#>V{D4D&vMTGJcsAFLb!fYHWrH6;WA0EM|p1LqH)H56$>MMzoM%lbWbIhZAF zQ)t_22f>(&!Ak$`yYGGY_@isX)zfFfzQZeB*O4@t)D=PYo_Bt1Yx}?amtX(yw?6m9 z@|(S2vVU(mYf@eg+l|R+qdIW3x3ttdb#{CC@UXwsnVp=qlSC9(7Z1JkC9kaN>Vxlp z@7;HPe7Lj<)`mJ*jv>UtaVN_XR$50ZsoV8i=Ql@E_a-`mGXS!EUy%$#=3&;W+1+ z_Ixk+l%S3UbKL+k1T6a5ZWt~ibqna`Hfy<2ZX3!==;T@L!~kY>0(Dt#D%LIIc$RiX zCuebEYqNBjHPhMFx!m`4Iy*Hw(?z@H%1gfMTfg8rw_M4D?C`eRZ$JPB5$6%HZD$f| zAkf_d{j(#f&;Ty@bLIdNT3zhTs_M>1p7{K$Us+^X*>_Yr${hj#gDW#IwcG)MBg->H z5VK?kiiiU3cCeV*q>gCyl9h$kz6Sk^M<=tZmR`PA_9S#N{LDC>8Z|{HvhHwIw800P@a~SxOE8Oi59h z=E8_L-rSKGm)4fDB1=iNZWBAleyN}pOfShWCfR00)+%O_a}7Fz6^}1hNakG*i#G zJwxwJ5+uQnH&9fwm=qZj(I`>3iw@c9I@G#3gB_Ix=A3D})y#B}(UMKY93lZYr_pF< z1f4>&t~c*AB_AsDdE18Zx%JCl^F@nSTzmGR`yYSb+lPZiQUeLLn@(y7Ab^>tzx=CX z_kHv+wfcrTF8}Ip_{Q5_aC3jqhYRcc+4ax7>&_?cJpJ_fvkyM{aG(6v^vQ+t(nG^T z)A4BS%I-IR=W~_{uA9W<#v7-W_g~jvI?!L-Uro-N8sf8_{?DS)n*kxtOQOZqOMKRe z&1~oV<4E&{qauLqx|i^Rrjt|?kXzY%McH2&tX}4vgZVNLjib#IJ7*r9?3{h@Qi{8Q|m+X7&f#Xj-b*$Qc;`n0^7M*-N z8T9%-p47*mnqIxH*DGkW6V@)ZoA0>vfycH#bJxAU^#_0S-T(YMzU2$P?5BVJXD8cJ zx7cmDiLqkMO6g-X3E(=JW+A57R8<)COcekqbF|n;W(OWJ1t`I>#6-SFNx_lEgw7MQ zOlH{03SK<_^vTB_dh8`He!*U$$B&(?hYQ`pz2PM{yx}#^z5ePWE33m<)tH(A6a^7b zY@56s0$@GciA{xw0MDFiE_U&7SDj2jWzZ{AoBrXWcfRwZpID#h=fC0G5FzuK%RC|w zk)f#yddiC|Hp1R9NKC=G9F2fMMP}QxrfJGvaq#G#rEY%q1*IbcI}m|30XmAcDGLy!hz_vtAObf6%MP`w5Oh?-*(Q$EGB${? zHMP}&_A_k)MZq$+tjp72XUDeEwNA;vOqx<{TjO@UnrzN=x=~$y&GP}^?l=8Oy}mPC zT?rsq6u5imK@sb9-9wLVz3VLxX8HJ)U+~f&{K@ahvTWnb`lFwCY`nf5G%W91z2yZr zf9}`52EG+R9%k^F1E_j&zZQPq(c$y`%!kGD@QtzEvn z=kgTlw%Q?=nS!C>Gs0rK08K{KRFuQcV2_ziwoZh0>U}xqcPP!_p{N9CKy#=W`vX_M zxWBLu0NUxs*<*J>-Wjf40submFRfkr*_Zdd`L}-QXMWoC7R%grBm+I@#=5lZUUNfn z?y1q=Ju$uM>LbUWIQgbO`2CxH;f^nP<5%4O$VXdNteO)i$HTqdo`;7Y+3v3NEn#zBhj5*GNi#^oRfb+{XE8Uf1EiEjRNIJH5FAY}9r`Bst z{0f;NH-F<-eesu8+tdH>pMJbpT?<1}oTVnkwZ)=XB*}OuQZM69 z7n8^wOa!1YXwyKi$AuS*!o*-{Yn(4Eic_gu1Ot(I%7cu@-aFm1@bJk~FC)6;h0lHX z)XB}K&wu;ZyzcwI;|q7TD~V})G+`nDm^*sa6o~WABA8A`XOjfL3k`ZuL^4G~omX$7 zMyM+b{l}g@_49xDw;y}x;gIT=KmUaQU`%eIllQtV%Muy`1c}}|GSnDdmVVynTHX6&D?yiXmm&%5bT038{wZ?xZ4HYr8f{_6T-gLPjQE-QL#Yc4AVNmSSg3zFeHFjrJ|4_=IjsL32-6axiLn$(Dp*~O^9 zKuDI17|bLAkQtI<^o_X!l~GolOqHT>MoM5_N!1K7N9>`UnP|z<0jG$Cw}j9}V8&Ss z{Y*6i_sKb!O;rGL#7QkFS!N`G%xu)sRJq66F&#}H7_4MS$m6J)p+k3WB3rG`oeX=J zgr?(m*3dNBEZNkuNmawb%8}deXq)D}@BLs#Wv>aHUg0|(Rh#$Pm?@h6<)40XWBaLJ z{{5eL(aT@#klyqQfAYb<{@84Dvaxg4F)uIg?Oe6z3%~94uYA?>U;LUEJo&_cwK*C-)ua3;#2QF@yTCoHqTZgUcTie z2XA>*uh$V+z3kfOUjKsEKlk(ReB|z9Ww~(OzGBU`7W~FSZ#rxGfA;VhLmrK*wv(Ma z|FD`|fAbytSN5KGa=i*^VYyspdg{c^!5jDW3oD2pIX0QZcy(DmethR;pZAizYX?60 z@sHm1sgHcNiphp~&N!LGe%}py*{q6ev^kZ!sTWpP-|(eh@^8NXKM=b|AAj%z?|OUH z%!(4X*PB;;&gVYo_7{G^SAEMJuXycGeBW0vjmv&^{KRN^f3dhw_Pn)|8J;|~y1eh~ zrI$T+=FHvq+<(XI&-v=F`>J36Z@)3x+{v0kR=U*9g|ON_=ZZ34TcO58k}tYtXTV4R zplq!RlA;Z=Ap+QGlN^Mx$eD7AA+6gH7~w- zYhyC2TPAYoE=cW+4aAh3%gcU}FdLmQO9*_?d^mSiGk|~rVb>Sl@t?orgTM5qzn|69 zLkIRu>S?E&0|0w=rAG&pds0zV0_P}?L5+wVAt)M(To`L5*?VA#VmkTcxy|jVFmmbU z<1X*8bZ%ZWqR)&eFI=8E_DmUj$7Y6(JB0%_A`~(6&Jsve31N=EqMS2CW>O+cs!7Z{ zixHJbDneAm7CMiZtMXX{!`gW0ROlfaKW#E zs3N^?d1!Irt|yzU=*~kIoLW*50A}w|=eYtNfDj!ZwX?CF)`g>oQ`0<*SLwiFX zhuOSd!XVm41OhU13JCy284-b1#e+FQLktRvW@?QfI(6bn6^)p+NEfXTA`$pXH*L^s3i!9%FqEsv=m?xVQjhp z+Ci8m%1APxCa5YEPR63=Yh<(zkwe{@q|;-X0Xfq?`3%Co<^1Tax9z#?(zA~|=uSU9 z==MnJ&T#1ZeUM~GAlB>WANb%yAAI-6zvS!Q@QTlQ6#%^X58nD$zxuAlo>!>$tt|EW z3#VND;IUJue(EnyKJnBSeZ}j(`0L*Iu@4=Glkv)zEZ_KorM10zQz<(zFxZ`~ax>XV zvG!S~=qf_{ZB7>*(?9`5B%Z>|LW(a zkH0z1nvGMFxYS(_gTMIb`7@&lFdjIvd}MJC+W7RTC-)z?RN~^vQ}^%Lx0YwPwbLBE zYH6u!0JwR2`_#tl$Q8=~soUch-SHwbd;f>t?s~eo)X8%{-I*#`zmw0-jJwMP)$J?< zmlePAEC22G=fBvJ{M=9dyFd8L-^OMNtjoRLgAbkkt3UgV@BZ$8{Uu-Z?Q44ve*Jg< z#NYnTzv_2cDJ9W8dj@5Zg`lCT4zK9R>FV*V^>=>klea(bwnGOFKKHigzU6Iyzg(8_ zd<{(0ZKRt;tz$Uu%P#l&YO!f|!{QJe6s%paQNUSE?5LMpFS8iT$fEk*fcpbuwr=6t z1Z`{UPu%y3kD2N9M~_~$chBped+4gm4{vPjc;^s7?HL9(0->cW>pNGp%_ucfG(;pb z#kt&76%d0$?meiUI&=27|Neb{^MQ}AtoHXWt)OF{JM;_yrl3(QLq#H!Xy)?*0YhwE zo_qE&1w~>8OctYs@hpyJ+v93yQWKN+j)_c7Ofd5o0?&{$$~!*m9e15z`3>P~z|GTtRM11DYUB3n((vf{DC;&+vVG80*Nj0K#2&Uu^0W_(h zfg0>~`_imRNgi*CPHNA!wjJY<4rPXDAO;+!c-AZO#$)22cL=(pa0zF&uub~-*>!Mmcb@Sv`{XdNt07y zbYSTw^v&OV@a->p%^jB=y=3K*!P(6duX@ex%lmn>oi6k(ObyU*?k$(tI$yFY053?i z@nS<)QRfXiBu2r^`P>jwYE%VmH%4tYF;@j$0c20#_t0o`zFb`JdH?M3JI_7#3Ga#X zu9&*6k8OJV-bZ%WI%Q}5#GR@yE%W@~Kp&bs1=#`DD@GG{ViLBRS*WDDcern@Bue$P z-a3Ek;DO6~gO!+JXHd4IF?PA%$*O?4!*gdVKPYpCs+C^H@7;R<5zn1F4&M91Gio)7 z-gh!bF;tv&pvG!Ex#OiTz4@lw5b)%QC;sqve`^$HhxV_zOcwh6y?Zdv^&=mA^L5u; zcg-y?zv|i>m-g)Q>3lECwGL)6?`7y=Jc)i#m)$jw?x_c1ONyys~NhMdIqO!=7RH58L?K(3qrz1RK-G)*;LPuF+^A9<7pF9 zTv=Ux{&R1><{(sci}>t3`S~RVhG=H&azru_Qw5q=p}P%w08oQ6_nka@+ebeAq0ij= zu20>!I4BEtVp@7kb6CkP;XNnkU|tLpdhbmH5iX|Z0uor7D|%<6YIA*5s}d0+0nX)a zV8nnt|J!c38>0gNMpbp}2#Fj@8#S?sX;Sr$!Cc|Uv2zYgu{C4DoJ~wyQBiixgbE0% z0H&&>q6tJK%RMoqRv_t2)4_fFUU2QTZ4^YEyB+Z2_>Ktw4`fc80;pNP+i^r{bJ`a= zvjlTOvjlJ+cBg}i0w&@N4RNwH%P+m@u28q>^tUeQ2RG%;Z7a}*h<$K-5c3RjHt&oH z86hcZQb;JkV>L!7OwB9-IR{4eQF&fx}yW?=axA`Y~wU$3&ZXWH=5b(smGSHu;9#A2TQl#hQj%u{Q8&N@#5zK!1}RskA2|oY1$a(-73pIGkHAZ#s0NxVc#}i9gm$q^MMCHcIlC$ zmp^~s-`xA>SKd?ZdAfh|6$dl#QcA=g!2r~VB(|X*7v068?344rxceM6LjnabQ8F_{ znbgompY=>)Y_=^UNJhyFLCDY)eF<3K|Gu}r<-fh73X^~FE5D8g_q^}VSlZrTErvE@ z0;C>vKg_+sc%!vLS>OAr3E=!f8t+^3=T`SV z^wf#*tnPP;Yp%Q+%ASzJdSb|AdP!_-Wjt|EmU;C=( zee37HP*XN-D?*%)y+N&QD*$kLm%yd-JFyO-N-(WxyP~4x{c9 z{>-4lkmWj?Y(0I?aL?s~eOE3nbw^vxKiq}Kx2x9l@MUZJ4)mH)lZSS|a%4fbPCkW* zS6+4rj^cT#_FuiHt0^i!{=n&3C5I08>o)ZkdbMt?3@=+;Sh@GUPoF$_ZeedvnZtPI zO7lgj2G(Izoo$X*R+nzP?YW5f;DdMG`KeEKKD&C@Z}P5lTDN&tsE}c{Giu-Y_J4TQ z>%Z)}TVAo}z`m0wH@73GYX{5SGf!0q_jK}BmzQRpJ*y{9oq70?N1uDkjW<2-)^h(| z1#+llX*s=0CKpD00N-;g~I0!c%J{ku$z2riXvv{ zSXJB55IJoR6}RQ57*kAvyP?X^iM~Fs^2}jTz0`SqJ9BoMud^OXVjo;Bx8Ur!>QW-4zBdoz!o~N+*eJO zqA^Hsm}mW->*bxjd-{hiF~sKlxh%B@zw!&0U-TkoZzA)`qD>)<+Sk9>cH)fBwEt{PUmt z!}r|zU@tG`cQ!&Zu++@vImmF|M*XRi@v$+VoKo=QdK4UtENzLjhTT42a*4~c)U@OC zTj6{gAKZ%foPs32mqpW!hu?SR*i#!*x6lcD_Y9Z!9-QrLY@a_vx#!I430lNpD&tCK zu^z!>i$VzX_!t0WMYrf(a_?j111Cp^u35VN=B1K!d~)M5p1$%DdHofPJF`zb_=Fh@ zy30^>m-i1>x{mVh)@C?A)9!L7nNnW%dbqPb>-qg=_W0uuNyl}2y>6bJIXflgvM@!E z7Pfa}Yc#v^iq8_`uO4l0+;i{yLzoJXnB{ra?PnWXvns0hw$tdszUBMw{n(i^r`HZ% za@9>&6gj16C(q0dA1GiNqe)UZm~AX{^4a$6*pp8q;?e#4miDiOLC!}G`h!QX(}Qjo z&?BQbR6}mmZ`Cj#+ilfw_u25+={Vg{3nT$DbQy+beD&d@KlZKv;yd2>m7n|kmxQ*4 z!l1*}_br8J`oLoko7s`WhmUPl-}8%adgAP6mbnOUVgA00RwgFb=_6v>Y|kYZs>-OH zZt=^1_?O@F)BjD1{h$B2UwHGiSEm$F6&!T-49bO)JKmKUml+fu08A}KQ8CIK5C9^V z9ViROEGkH7M1laSlT+L0kFUGTBg~bipoD6snhcc@OpP6qY3{hwFMWqPJ)dXZXXtRw zsR>er7($9E)orY+Scf=kQ`0JFmSttXlNGu54v|y4FHzIBI|jVQE1vY zX=4btU4HrZyy1&(zwXLWHD>Y`qv33~p>rVw=6`Xa0qiDaZtPUES!C~k%nY1Z0`g8m zN=gMHs8%!Ng7ZS^+GoA)o+ItXc}`WV5k<~$c0BU+d3I-Pv}rTv_c&ZFu<&3kb9FR3 zWW`9RqF}(x3c(v^W-~N13ZgaGfM^l$Op+&q9nE=YL|_3istLhaP!!m2;1GjKGGy|V zXwzsvM-?CfW@E%UQO2qY0xW1#Q3660H4*XSq-#Sk!Evh@5)%qhm8gWaF+367p)8e1 zvp`wSc^6Y7VCXY~!kN!tI+dLjknT!^uaw;=`09jU~+BJyaM zLP%|`-L4sF73D2=VK4VDxvFa815cWw4mz~9I>@`l_d#(OVDh>0+F<3kBSEdFGNXba>M6TOXReVBZ+gs8=4iYAHuT zcGdRI&Zyb9*d?^4N(;*ngRMWhRlH&V0O!u0%1g-!c-FL0iyW&e6$=ZU%;KzZOslJ_ zMOHla_(R7}9@o0fI)$Q}gr;Yh`wXBp112pOdyhWy^kWY_xNqO9*47RQez@PyX5(h@ zilxDzH*O^)U32HV#olSK#~yz=Nm}eL92y=PO9*+y!aEyiQ-jN_nxGni1IGrsJ3Un4 z&9;amN$Zw5>3Cw)egW-lt9$10r=Gs*$fZ{vz63I-$g(wtq<$RX(uGH92NBm+R&Krd zns@%i-~Gd#_kZ6TU%9bWC5c36^NLr6h>K#NnnFE6_NZWBc~SoC@BZbl{o&ue`1yCN zt}JhCoS(IIxzfw~T`r09-JBTqo+Uz305DJ?B{XfLsihD!HSB$8lQv1fkO+}N)0}_k zL_Mq7IW(OjTn2#h8QVi6L}LysIIG42jp4cVIl z5^|O+D1u@mO%;rQ5S>D|SIWG%sE}%rl*Y5_r8nL1!{7Q<#5Af}Bo8(}Ufs{vp@vx93k=1WcmZB9g;Q zV>;OY2^J2E%$#7?n;|kFqGb*oSWFm`1waOj<{Usl#yaoTLR6}hdjw2WYpRrngmdju zG$3HGP@6?$M401d6(E|$q@c7rh5!UaF+(&q(v(cVa83+T2WAG1LC(8LqXN6aPc>~3 z#E=cTXpLg8mlq|FGiy}P6oH&aKuu9aki8LG+YGL`%HMLewgC}vH(U{l%sq1d@w*>+ zaSCPlF5g=-c-EQe}k0uhKC`&|C40%RIHk;03kl`>- z5s8x!7-Cj%k&}TXjWMM*Y0)hmd53i;_t2s_tlHR3jx?Li+Ua;~W|=~kq9>@wO_Qo= zvwiGrb#S?4>B?}k=oRcJhQQ8ud;MagXfefJ^0CA!sEb+@+%%{O(9H2Jnwl6qA|rx> zpsW#Fq3wu;#wN`s6%j4;`p^WE8d`;nF$XAhYc?`7?|i3|qmu{kJ+}SII~E2V#k{U# zQ^&bvkI*Brnu-XLiAtx)&#v#h^#h++T3Wc|$YIB>iP17EdmY!!nGDqC<)jH{QWJyu zQq`DJND`B@l9Hh{sZDXxMl+w~#1vx;5qy?|Lu1IuBLWbT%hVYncxJ~?`Vg!>8yFOCapsGP?PDTQdM7;+j)s*^<5NML1QZERO zq);pv4WUPFrl^+oGp}pd6z%WoFs(7pP#J6+WD&AVEJH)ISwaWa8qVcuqUIf;hgq$i z%r#&>DFZ7|07XJl42Hnm@wQb#YuG_BgJ^`N02U2aGAlCfE<0Z$qBc88llY7P5n_V| zSmu_Z>-4}3oL~O@*Vf|=iveL5Xvlef{>=D~-t<9h7Y^j*$=L~H7SOY!lc!_6<@4Tf z+w}`KtrR=k@LT_IPrW0qZ?H3~#i2Cj0R=(>Ml?Z0i-Foe%wU?p;PU;rF^L~~ zVztPN!nD!6!<>;Navt`t zl&3oj`EXk{+x=9OOEyYoYE7fDhN>d+bBz!gNI`9v!9dufDholhtZu`(%@e0jJm;nx zui0}re_}J!iDj&XXCX|@u@Bup`Ho3{*uUn`<$s$meE6QHzUF)X%aup>-gxDa=ih$S z;e%_j4dZFu%?k2aY+H#9DuW|r{LjDp7iTv%FTeE2+P*zzR)>mKvW_P{@ET-=B#DH*CENmK{`0xp|}`XGp@iHb_2 z=*fttqiGz~|MKg;KIf-nxtZY=1@sf zExn#&H1hE8WDq4YVVjPlF zfYx9vy3C|WQy?zqYDFWmDiM*^KptJj5+Va61ZSu-FajX7Nozr9z&rzp24W`22Dq5P zaFu~UB}s(Th|d5C6$nI9jOonQ;vLUld+n>|W@w8Ml7inICz)BdpbtH8OeZT>tnPi{ zscdlph+-8dC)Y_;8%dLv z9OSE=)gy~Bq+fgMd%yj2U$t+!3_A@dpdlluNp28}OguPMt1DnaGb6(``bGgjO|=yh zO`ga=xu7Cy*RGVCcGl#B#~vJ(h>nP$X_AR{2Gll|s#J}^73?z|44FKw9qy@(&TYrJ zpyB4FS>A0H_V?>)ipg97{U8q?tM1vbg#%0V&U%o<1e##u$sL~!m)90LaU4>b*3)va z=lZ_g4(CU)?B?uj{&iZGRWse#XxEOcHc5KJ5;L-@VzU0iaA$iIV;rokE)0e_BD=hv z&9X&L2yF{yx-cm7(vdIGxpSMR&Fqz5^leu^@ALNT*|%rUL7$iKk5?VrbLTdWJ-+i< zJjuy(n_bMidAFn8)79zOUUW;@pe|+F$aI>>TfYZHbI${?XLN~}ayFur64Y%Jw>2I= z^zfr!@S4|Nvj1@2(Drz1FzizkDa(slH9E0AY5vEqcjQuH@)_TMcI&?Lqrdsk!>fP# z-WNXi>Tmw)S6_3WpZvgaR#j(HY7nwvQM~7q_r2>A_be{;mzEZ8?j zQWX`|6p{d_L7WB-veqAd`}cm$i|)8_v|Z0dhlY3&u5fXN@xQz-uvZKLbbi!J|3lV& z&LOa(5g5QcgfbZj#s~t82p)9NB|zvTJRSLjX$iQ>Hgxougd;>(XZa`F=_>hy&XYlo z7>(97HP&41dWzga+JU%$z7d<%IxHN50zuA5NX}_v=1duFDwGjsXsR#;TtKap%@Sk; zW0OX(4@^K&V3@&j-z2FeOASg;c0f!inpwh_gql6Cf6dw(Ug^5|MROk^UR2BxfTGBx zJXkJG(`GVS$d?Q(FZ>(cc*lX2;Ht?qu|Ybtf8XIld(Ms~Xh6U&&xzTh5^<_pEHvw9 z?IfWCP)JkB3xD_VV^6Q2zkL6d@!${~8JapGU?6mutW7F{Q6VT8XaN)hNdf>=z@U^7 z#5sGFwV%0KXJ=>E9JwPk?tyb3Z9)SCG1~ShU9!KJde{o45zdd=#Wi2{P?ANF2;3hQ z06>gc#wQ<-b*hI;;7UehmGsC4-Vxrvrw68<@CEyqwxqKTt%0`D_8eaDK9^bh z)UnO1*MW9An@s_rJLtQVW;0njxL9d1l_gNVJjA(Z&3Ph^K-4MN zd&sB`#5zxtQo@DK0#(6Oh^O+p=22ypA%nR92)@85gisw=PT zEcRwuQyl4`cUh-DS)ab?_Dv7b*#TQwtSJpeib6HQEL zMa~^0u-LZd%rRKf5YybOnUaROm1Lqu2DE4*xHyl0s{9CR&d|=PY;$XBrMWT5d z3kd=2g7jn;8To(ktOq#n2&rX0TOaK_`qYU|*K0!biiVEBEORCZD(DbFRJDKjvY4RR z9ub$AG_?WCJkDBiEef|(Y)>~%H?m6T2r^<&WhO&Vn716sASoG=v4R@Rli9?e%X$@3 zH{nAnr^K#sZUyl@ZF5VOT|)ktgv!D`my=n?Lnqjo*e-Cn%0!AI)iKxtavh~71|ChR z^g8z>7-_54cW97uV@6t}1V$~{c8kd%XE-m?MGAn5u!3oIK(1q{hIS^R$T^sUn815X zb<|#Y$v1q(@*U4@Hn%K90Juo#M#b%wf@@_=Q}}Ou!Y`KfH4Mo=0pN5q!3i4-c%SNt^1HB7NYl2XDFb z(9)h2AOu5=iXnm#5~@aS5z&;RRgt`tB7266lzD6#Pz8jn1pR#1$ zK;~W6SzTFq&j;V}lRx&oM-JxOPi)+M?6{534pw>pVtVp{ohLSH0)KE{+1qnf+jg_% zh2Gh7;ml^eZ@F|uJ!)L(_ZItirXy*qw&Tz<56XVAG{4#?^W0tZ;y&t*fp$8s0e%Cz@+UZC9BHIYeERVf>E-0EO>XnDR(Xb4FJmE*LrB&eulY6eO%B|}h6qj5W# z)T?#6w2ccQg@{ks5&}_;nt(IJOr$fb{3JP~4zZr!%6uLOg&5d1XyQn77tn&IRQT2b zmgVxnL!wEewkZM6gL@nsL9~9S+`n&aVc1{lcJ?j|53H^&E%d;&ZsT}Tv12eryGX{k zh|5F7|3l^NeC?8`%*-=yZjX1SRh~O_$c(_`j2vidszAWbSptBR_m*bk3Q|I2(FQ_r zy{^P$sZns1xM|e00-b~mE9SmO5}{{CEiDC z9%-BFe7R+ECbbLX2#HY*tQDJTS~OPxidql@vZMwI1O#es&cg&N5CPF+5+DR{U>>11 z^I)?8Vpu660emsVC@^4#I|*XVQ#ntg`!pxNK1Hr25od4?TIu&3pG9F*RhwkiaZS^6Ui^k`MzV1Io}y*(vjd6TWCnhy*dI zC^P{}k6mZmtt@0~oqZYaVPHn_UbaW(iTfRDLt-#UK*+*IOyFGIrujh3IU20?vH@=# zn~X+MmUwn6n@yXtm+ie|u^I0uOqd`7j|DMf-%uNjDNU2~T{f+z-Co%%vz=b1n9OY0 z@0p@woJ<;E@!i~KP)%fe+$dyZrW&?)*3E3NwAx)=ZD-qMp+)IDITA3XCMr6p+ZmA6 zRrA)rdDC;Bv+Sn!*n?*mazGxq)zyRhI_Kh+ckHrkanbpV@+{MCr)`64n@(P`swF{3 z1?73!nP5zzZrW+e2DTf-;G8e=exGvRoY^2^@_DbwFWtZF8RqIGGn-b?Os~4?kOMln zXX)B&4!`}wcZ0+fV^NmJM zIHT)>beksg907wPv$@uV1h6#~L?uvi?Jhi0DJE0#fI0I5k{B2v5LTd4xa2tG?aN>L z>V>__JL_AmDWM@CGduDbyFs^HT^bfy*74qXFJ{wettx;-Oc(i(0P{Csc!4Aca52)} zE=(|YKjY%5Zub=d3@>~DV7F8jGX%h7kSUM{CTEVV0x$sq$O;Lw-rhq{_R_|NWyoMy zw+T(!2`UkBm~C#RjEkqv6K#&fe2p+SlYil#8O$_#+VI%@;iLDRv$fuV%NDwb<<9s8cO3BE)gj#d z^rvOeOe@JAk( z-|^$)ILi$UEJJf>2{9v=ERkVGtfpovYJh0S4FG#j%rg}S;L#M!01E@CBB9JqZ|P?q zzUFJ+(7Ez5sRNP&Ft|WMQ_(1*rcD%$k>)U4cDrBp#c~!d{vUYp)P{3?qwVf@^F0tQ zHvI77t3AWh-(`1z5t=1bL35InQsR^xgvOA784^g{a;MYXbEqEGQqS0tnu6<)3k+VQ zl3-b<_q2F7>>hC&1B9+4=TQNf%q|`+8Gw+)6v1RS;l@A|fej4Y6j;DK=po=wx6Kis zU6~gjnQZOJa*wzX!!*Sb!4y?tGsWYv%@6_gbW8(w9fwUcuRsh$CQiUyKT|U#at@LL zCbDQiN)y9H#7e}ot$O$9EDeZC53>rJU?>0?#zaMC1UenpgIk|_<#&8p*)QVG)R2I{ zBH~4l%Wj1WDCQhaM(M#%oQT|(y@TM~*>jIxIov}CbC0m?zM`4Tf?RuOZJ6gOlx|ejCjW%qCdR!^sd+xgT^{;)+kk9~4CB@`Q2;ar9Z`TLP9Emrpy3;A@SsJVs zh%}w0Jd0A-m-PGhoN;N=K*=#q3E51Ogp^Vmx1%uYuk7hS>4(D}6m$EjW6lQ)8RhE4 zA6c>_5ajBkd$WvYEyM1w8ndEHOpIh~Z4B*BG}O|M+G?llE!}qgrB|)O*Szi}vsrcb zqbKe$26=*v=$1M2-M6&;25|iWd4y2UqvQ|5#h&oo3qblw*p# z*je7Uhzx0drZAdAkR+sLeUjxlc6%Wv2x%@_o;9&hhp=z@m`MwBx~Pnj70S z0A#?R!i0(>svZ%M0;=pXqO?!|Ff_=S0aO4)K?o6n33$-?)LY;G_$Tjw-M|0mt4H^@ z(+MyG*fU{A^JshkLI&E+Zn60rg85<7c5Ud-{BL-19|W9R(4Wakm|tNRw?W($^j!q7 zA?!j@hzLw6CNzS=lYz6{W3_0KV$Xpp#x$Ft+j z*v5(TvrvZuBg>h4pFaMn;~P({*P*Ub*xBAXdE#{2w1fy~92J@nW93e*!)T%op^c~t zZEe$9n^qkHfD))7REX2M>Yf@LI(uO6xkEO}6G&C1 zNhEc8`ib%B2e;bkZ2m0E`?>3tj^fVQ<{clJthai@VgJHI(@T5CVt~N1J99gGa$~EC zhYk$u^*V3$E1omBc>$72tRo_JI-QO6a}Pau_r3%BUh$$k>Pgik8w|V!Wm6QHRRPG* zbhy|*aqh7WFSH@$c`AAdStWMOj|2lr_A>+ zl+wudC}D=3(#@VMqR8?Ri!POMQct$Gw$D7h9k zH!UbxcR0xNLSqC_N@4#(M{Qm~rQLhI^FVB%^WQizGI2BAX(pQh7^T=8O_NHuTfF*? z>we&CUi*vR|CKMh{rZ~v$XQ$Mq}U=+0A%J@Zuk4+7w$@Lb9j#Y>c`!J- zI@mivLIWZ|CNh!KMgY*HZAeK_o0z7}Y!=3|cB^jMN&TE@{_5FqPjuw1QD~zfWZsnu z$r;Wm0|8V4cbUCt3J?td3Cw0{^A1e{1F+V#RtqYONM=Dr6&Q%id~)vmJOATv%yz~_ zrz8N6j6_UKNCdds@dCI2!JGHME`Zjb;UF8>yjrz6dj^fncB_9w1GKq{nH`s(U4t_3 zlz{VOr1{l4Cs3M-mSvIWnJGvsNx^lz10+YeT0pbT>OMDE2z7{DlOj|#1l8UE4K+2c zQy^ugVLX}q%^CaFE@!ipz;ZHDvIIs6j6_X^QnbndR8fg`2U7q*q5uSfSfCwXdP?Ki zG^#ulsw=wv$7i!a$9=_hS1#t371`!=^5W(4iw_Le76-E`&Zfcd#QP`h&~YEJJ825c zeE~VC7!-vUvpQ-73#x>eRLrmkeg=AZS1#@H*$SS!;F=Z6I#t>8tg9s#R8UO$y%gBgbi~<2VT@y8QR1qQ&fbW%Auggdxsv5*8Xx5$ey4o*` zbH`-;u?MzqyKS)VVDa>W8=I$RyL9E4*h5xy-t(z*_nu8Jxv_lS6=iRzqn%VuVY06M z5)e@WG*qq)a;@v3b1%LE{1ne`hCwbMxUf2KK%kSmKK|ibZ+p?T*FX0I?|A#swKb6 zuJl$qp$#agWQ2gC)<&CDvDX5CA*fhtW0S0H!)%sRq@U=luByn(Lrt=1NuTx%mBut1QS|W9hEJ9mF zH_kf!f)*0Y3_vt>2tRs$cFW<;tBOgZs0!fKYK}$>)P$s9W<-%&z&cSf16FTyON=gn zYrzDy20em&9Ad3DCwg6x^)6q{jttVVX@HIM-7>pk&+x>~RN}1bX=$J@@SU@*j3hk; zwurE7I5X!cIP`!@YNS@1CD2^SV2*y!0{DtPW`ujQL>W9eg_0E68gwLT3~(P|qR~83d*n--{{ZI6M>dRPot$ObqA$6*T$9zIV*;gFE1^U&jG;u zY?$@)JVVPIxyJizF4?!d&}|w;L=%}e0Guz`=b8dOi_{MYDORe<=UpJS`TddzkR&Pl z-1`myq%fUB6<9bH@Pmt7{FvwMq`!9veVTLC(9&QK+ZR>2dtldA1j0H6SE zog5}M-WWC1>Wyu_8eIo?hf--)WVtaTfcKnIWOam{R;wX%LNc01Dw<WMQP zWN}!&^ebO=+0EDXhJ&`Qhgdb>j8tW?|3KO6HydX|U7G}r zEdf}^XTC(DAY($8Qmd+-IrrIH>+)QNw{v@~lWjC`!ene#7)8-0p@;?`0Yp?1oQZ3J zXAn&&pds0u{X%L2*}+*XVIYJya5@4h3%5$M6La@ zcv9F}Ik-GLwOLOzB4)@CRYul;QDN+n6M`rm?rG*A3HY41Y8a`mWLO!XswYT>q_Fn+ zFJF7<9o7UuFq;2*3~^5I#Ai~r(dNCq4DFGpM-M!GdUZAX>aTdwE!Q6~fwoQE1p-ho z2IGr(cbr>n%rck1^f}kQ=hOE+b$(-PqrZE{KfL$_FPcmzS(zb%B;(vk6Eq|w0%S$m zv$U|f&{O8Lz7rlh?lP}2S^wBXJ5~0U4;t!ooDao{!=a?Rc$KXSrtpAVTzX>WS*iEPul*hF2WT6ESsr zP186e_MCS;7dWt&^X;b*Ya0J>2vem{5HDlD6Eqg>a(}T+Tc7^mUmd;j-@M{;UjM+u?>heEBWB{*s+iiO z462CE&nu}|hlY6KtSfYN=nkR*Py?}`1F9lH6YxAcd7GkxdZzVBV{ z{AdcPkF>IsXU;wKzB{)ceCoz8e8s}vmDEN#6gm#=VbvU$_JOgQ54czt1n&bWjXYhmLEF5b?W%W z-a-HT&UmR)uI$}^ax`uU63X!uHiWV3uI_g`<<5!8^kCO5I+~hpiF6QVNSR3wcyEE1 zETah+8vtxpSTK4bN$M~GIoKW@eB-OG`WIiTv&PzH*A~9J@S;c$0OkTp5i2_G$>a5# z-tv&|wmKmeV0WJ$42?U76N{q(>1nm_vc5B$RiKlAVZ z^MCC8MyHuny?u+4teMREN0-C-+3d^~I?lY`sixof#jpIHuYSWsV!G?e@WfNlFG6T( zE9MJBV6=jU5Y$Vf&IvnP`!s?yy(nEa3zLm|*GxvPzL(hHDD_{HCFZ{2+^6zP>JFIvwUwxjQ z=Bjp399RVG=h-Z6p4whmDDHgMpI?5>^KO3OtN-=){K`B3;&&hV)X(?!?sZ0a&N-Q? zfkJ8#q;YAb%)IxADD^}iKRvnm2z0w>VKfP|7_s3x#04**uI+xE*|_L9!tn=U+sAf$lokXS*j zj5;Rg^$8FpE>2Lhyy&r$TJGYp5bTl#8Ilp!Mu1C%u zJN{Ka^o<{T$H#u}x8AZm>@SyoxyS}N<%}||o<6tHOeZh-wy(&#MG|2kH7$H`U~T2T zM;`a&fB?#jjKS1~M=ozC({^KG2`$Ss%m4tyOq3J@lgmqU>hhcoH3ld$@BE_C4DCOk zcfU7sMHjj`5~zU8e`1VSI_q&7uw|$ngaK-w@e1dTP_E=A$oGZmqbcT0``vs3F5TSs zJH;e|nARayTcgp5(+{8BK8qP4Hi|RDSFQH1?qqd}+n|_th6}yzq#Hq6)mqYMPT52N zq%PUM1v4@L%N=aYU^`jgDQhfQfe5R~!I$28>DRnoCR1%2B{G|Ddj@6~Dc^{=J7PB^ zBu4zLKl;=Y$Itx44}H#_rNQR*OiV9~o(usH@R`_BLj-dDPJMEt{=g$)+#cAs^zC2v zn$Lg9ZMpX;1Yc$`X_zGn!Ri2j#h}ZEz?gZzH0S`BGv`+qRZw@T6a=Os8{1+jWsdq? zEKAqVuualLnCC1ZnX##&GYA%@Q52-ElSN3$*diq`Q&Yf5t#JVK+6uy01X@E;49z(X z@83_4FU}9Gz5w5m#T6h&p^odPr=zo#I#&#eqNHiXkDZ!@q>CN9X>a2pL5WWCM^44# zBPFIid%E8FTbA*f1Li!R*{c8ikAFubJ?A;Ee)-G4sM*+j-b*e!a^-4|3g7s4r#`#0 zy}nfgy*=?ceZi&NN|zE*}m$T(u0guy3$uwbSpo zUJ0W~;3C_%vXCLy%Ivs-E`vMybbJ5f8!x?O@2~&p=f3cktG9y&mr`ivDvegcP8vn0 zY1U50qxm>6wJC1J9?}=xe&m~8bM5}MejC*cFDNRKg3gciMQ89VovZ;N5{XJ%Pt2?+ z`fwr06~PcNFNS8Onl!bDs9=f;gl&i`3k%m@b=7XqA0S2nKnpRB>Qo0vCT(&_V;!4W zJ?8)>wQQEIuH4I0zelb?ox}SCO-$$Ha|93(K{g?c%4P@!5EV!cQ4tkjo*(KI5<&|~ zfCw%_ECDLhk)+nFP00`ggCaOU2cT#U2?6HCV|RJsk@tM;FaFifY@a$;^m-}^0BFxd z`_0D@c2RbMMuw)4ck++F^HZPs&^^lwy@N}=!65H-vW3!jyX9c1bNqqF-~Ri5|K4|h zS{x2L*)Vg%(y_}TKr~qF4<;wipMLB~pJk|qU|(o`sws<_wwG%)N7c^@g_L_@uO^<_Y#>S;KtApt}=Nx4ZDs6YHDX+lQTN5SO~H z-rBimw6$3Wk2VE;+2O?(R^fO|qZk(lgD#!jY+@gAr-cME56NY~qm~@fdM%Gl zbx2g$?(is2@xbd|aO4}l6pS?_Aet*94KARx!4P(t2p5jx*6aI^edbKzP6x`_)PaNN#cG}Dnh=SpGON&B!E5j3aZF`rNmzQ)JuJ2B-+3RO2 zIXb5DW1s%;d*A!_uYUCxe#dwI*v(g7ovP+D@BV9RrqkLYQZ>gvlil^1OFt&`c+2 zUeBa%o^#XH2NnvN)WN_UvVrNYER)b^o`g|$myx`R60w1q8UZ0dU2OwE z(OZOb1iMrcAeZIk0wTsxOKK4rE`+$E8MqR#RNE~B*|C|VplSvNsZ~u$#Y|0urbYl@ zS@{ceacE@=qRrG(PDo&AyJ1p^fDdkFKywICMR5-kMl^oToM4&2^OntZO%nMpwD?0NTdCtT;ic^XqCL}{hh}1_c zA}|_C-2fW4GYt*4v^ZG&SnU7giS9A+i!4z!DTpZ$AW>3Am=mEiBPl8NS^A_7bOCrD zdI#7=?-dRYI;&;Qm_OCT^JmZIAwC|YGTUF^6-C&`dTr*eF3S5S?a8LTVre)88V7Nz zx##U*DZ<$lc4FILm?q1K28C`nMyO9u!$aGK;1`@F5Fp~zx_-Xz3to%PY26ZD`0=Z% zM6d*)W`ITp@Jt2?shK_a{V4T&8ipg@9W9BtARa4Jh(pY-G z)=>-z6A%C!N%8>H7*Gaz$GV>8_s)6fpadfoKmf;eu-Sx)G7~TZi)mipWbCXgW@WM= z5>dukW+kRfm5i-TX*)s-X@2mJ_aDm7uJsmLEPV?UW=&DLZjRPAr=ObL`*`)@>ye2Q zC^Mpi(AYS-&ES^yt`&@Xo1GgD`leM-RYzPFIkoS9=U)QY;?nBpe8E>8d-$oQ63NQE z*Krc-CRJm>xsyjK2mhbulkoicyw{U#wu*ouX*i@mRDDI zo*p@oZmvV5SH0j4XKWOv$(p)tM)BrbKdYEr$pNgP6Q_@Dom)S$fB8qh`i0;5iW@v; z;~7jxTN~@=TWMpE?Y6CEb?!3+IJ$TEs^=V9C^-lK5*nKA2C-kvB$-zP>Ca{LJ7j4L||qNKtP1#k&)0j^bClCjL9^Lft5j$D&&9#q9dG35E!sa zE+axTR?7@}&UKKYff;AO^Qo$uF>nB@KqFcLb#oOyXm_!H;?4)|d*_EbgT5sJ!;3j* zh`ZY#UYO|`Dmol*kH_1i%sB&eB{PBtRIphaQ;;*$b_%+$o8K_(uMLVp*Kf7yd{d9w zNC;wBI`T=>5`xUnB2m)b)#cJ>?JTMSFp?9XvNM_0l%#YNXRRd0yu>JG%&uSzj>$P8 z3`l~|f^(dA88T5a%~B=^u?*p4(mx%QPeQ)t&@pKOQ8X|@K_nt{sPp=dL36eqL7%`Q zbpRM`$bLu|V^uf{n>+i9?9gIQna;(8Kulqq9uqomcBKfiZik7Jh&2UO(TDY#d- zUN`UexzokGAm#Et=*xuP0Z*GsTK6>Qn zfxT^rXsCd0&r+vTmRNF`Q>Q?4Q4`Fy>!{#(zTp7@Wynzxn4(~+p;KxUlUmG30AP1i zW2VlNO9*WOQi2#M5 z6($5mBB*Be&{Nx#$EDS*hq9+*Z8DiBeh5yhW@&MGW8>sgPd;L1KpmI6X=+p3I&0N@ zmRyK!Ox)Iyve50gW{PfM-xH7C_o2V~<9b?mSB78k6<-%8I<~O3wsdswo>oFrS!zvO zam5YS4px?uS+|pu=SejM$QJfr0w6usIG$~tJ6*NX>G+pD|B`?4x?5g&_5Qs}%MMvu z$p|kyu=Gt|`jW-Pt^u@3##M+>cMtSL^Xm3OX@+RKi(KYhrkcPmhEW3%N^H!``GN>Q z6%E0FoCo#>kYZ~fh(=(zD|q(K7hr~rb8AqTR%zOXNoZ%Gtt-b&fEeR6=vIhXPymP^ z!a}5sMC25}84;40Ap#*PsH1t91bgqi$4;5$j?EkbGMOT(5vdv?14WZYqpAf_CYK=< zU=B>dD$oj2t#P-BjU;MdKn5P!%mQdIO(LCz{$ua|#QEc=e6OP_bJZ8%1+fZV)m z5Tc2=ImOg$QpIQ*lGd$^VmjSM0b9x4a^d%M-5$@2$b0(va;I#}(*Jmg28oD}=G!R{ zCP^!U{@U6ym?9!5IG2@0cX3jWWIW~~(?(PpEFy|PXK0Ap3hd^934o#?mtA#$3O*l5 zJtb%*GyooB^?>>xr{NoT^kNDNgh8!JsHVUG%7no4fPMsTNQ4DKMs7#kh6Qxc277F~ zx$02odHWYF-#6?O8xKEq&zTK$gO##>a%1b!m@Y5375REAN2+*e-@fbr*_Y)<51xPW=+Tl zWHP^VkpcjaSvotlCKjDub~KYw9jd024U{>KYP{zZUcGk3l}En+(gV{f?u@7BM%(Ak zZ$Ezg%)`e{fB|KE^uV5_et$Bp=VB*5u$Uj;R__=jAwn{4l4PWi&nhAu{9Gcin&Hn)=H z`OfL5-uvc1`KIsrF_(2-^1841UJ{Nt2TT~+7LJ-+{n8<4T|+2=q1vRB=5WMdM}Zq8<*T^*KthQ-S2%KRpB zp;5~Q^V1fK*|S6!V2C0~W9^H6UJToMf*0mkhDt=Stz$dQioVN=SXab3m}W)S`2r9_ zHG=tuA!ugQ^AEl2{?$XBLs#I|*|WaP`U^`N=cnuI|HA`gpd`~^DYR`J-Apf;@QbAj z00xvqAqqx8E)29F*P2C8k3^_Ib6cuQO5Q=IV5Yobos4v5rN_od1;h*lRm>bRfti6P zN(P7^l00IKFcCF?E;$F3%m7r?GDe;s4$QzTLq-A2^3BIiyz}RO`-^|_Uv>MvYC6Mt zuiQWh$#x4|MV&`l6{YtXLz~pBHON4d+B8WmwvhXM-K_6uzUM;FWjZi%j$2PdUW4og$FG+ON zV4fihu_b6I0YfgK6{(GKO+Z;P=Z0l>+Sb25fe$!%(Skc{=@PT9CsV6cBO-|@B>*Cz zxt%yqy25P2pwbFHx&u_ky-V5F`sOt0`_7Cy$UALw+3L!^!P@=D&z@@H75(f=^TXC2 zoP^qsKY#zeGB0P%xhJc{s>^$Ox~F$n;SJ6A{UheWPIO5Teh|Xx_7(rw5oBdi0qMhrF-^%Yz%fiJWW~2g95_t`QLydP}V7vs5$V^qfpIa70`T3CE!+JEi$dq!#2q;BR-Q_}>}tX%H2b!ci+vxULh z-5>sdBmMdx{P(Z>if_E();q%Z$-5r-Se^}1vUXIhZ?{zyZn^zA58wH*^Jiz9>tWb$ zXS30jS6_A6(JSx%)Sc5=XM1y}nvDCyP8&mr$q}y%vwe#N5~)HP<7CpTDk5A29_idL z5JCAO)#&1BPg6DBUMTxzf2o;nhzjBEl?=clp{aKUi-+=Jn3|cPm}#E%hzL~MdOWAJ z85k3XS^cRuee#R{?Mn^p7Qf4s z)LPM9V>wVpj?5a;C}05506`ssv)$1|G9^Sn@@g3ongx;W!r|9J%W7W1o7cn{|dcin>;0)XJWYKibcOnfJ~CFkz7uSrkV` zV(dr&t4VXk3vbwe!&R}4ggD>(5U~p3_Uo>C+ouoiY>iaZWy96U=BC8f&~sj(B(Yrq zKg7yFh%zcXD2G^CJ2pVy8&G0FH4&Kiag!jIDZNFfLy<2;)bQ`E&yH;V3xztphIak1mpr^b%53{p}_&?KCYbK1_D zT_3I}CWXvvBd{^UoX9KpJJ!^?Q)NY-f|?ov5J7H4MnIN`lod;GHKzy!h^DN&baDic zAmeG!?WzQiIdx$Hp4u}yd~Q5SWtj%ck`Em0I+n!c`SJ6h+uM5%+VWNX<4@MAkZLuy zj>9HOWzs=K;t+#eaB(5Mt6O1rTRcEE+ zA)=LP$-;DDVeR3&-v8F$@~`=tA3Sp9H2_yB;ohacIlbMA?;RYtyx@g}mHm77o;!8V zWaPmL&xAy`zvT92JU+F4HpLKTGhFELynqJ_5gJRoj!sqQ4bW`Y%4xg%8^AnF2k9B< z`dl0ynlTs_-JvhLak5RktJ*_ARh7`1S>74evn@~n1opWBgk}=M3=zQyP{pLX&^>zV zCBwDl(9~PUj~~3^3IMzz#a7rFXce5w8zo+~ckexQodw-eY|H>)k@Uqp%~^Tho)cFd zJoeb~zhEVOr+YAW#H#MnDv7uA81$8t8#C zM?q6B=>)Mcn2C1D*2n{bnmOAY0Xs7yjHWif4mA2<_x`_m&+$(^@P?oG?uFH*rm7GL z4R)yl^Pvd=s3FnwU-g1xcRyNJEp$AX?ZjA%3|y%O9uT9bL)Lli1)>B@s^UE#eBRa1 z|I*ijDwkrd9&tZ~@SIU#sLr|R(n$JEH@ zi3BpS%qsvb8tnmBWquR${;=qCdTL{KJ~n42le}&R{o%pkWV4QoIfru~4)Njv zL3w$QIyr|(aT0dMercgtTa$AeCK0EI14hUC=sBnYtqozAIoU-Vpqdc`0YbbuLMG3= z*omVGa__`I6)|fi!uAxMw>HMA0ss@!rj^l*C|6TRDoslfGoLl7t=89F$3YY`1!m_+ zz=E39HM9Z|(bHU0ng>KRmfb2(1D@xpOnq!U=gx8Hq}AmTCYPO;TDDHrMG+AZfjfY+ z)>(|VEZ3(`K#9Z#m|4?fx|~<)9K>U^4%Z#_{ZLN-`9Na z_x;Ns|Jm!F_u{ww!GAw_Y7z?c=KNwgt>gAKukOi06Wbl$4A9N1GGBb)6Cd9`|1+=n z>TlR{;LsH}ea_2Y^Qkxe>F;C=|(f%0dQG&5zQvsC#9``7)%Wz@8l1>om;!=vikAIyT=|W zU;i2aYXCO^Gg3i|4t$K)Y;2tCEj@f*227XbH2^rVMFDJ-9((G>%pdLM>sRmF#O2ec zW3|5SGZeEZ$pH)SWY)AXW$bvKuWX>!4N!X%Kx$DsHcw#{M+B%7O&&BTF+fhJggY^* z!pvYysYC~0I(L_;0#X4?261jlHb|h}`_ZwJZ~5t8`=WpMFUw9Yq8ECCHn;E3ER_*Ua{rhA)gncp; zh^i`@%;xQRw;PlK-Cn-Bx^Q^k;NbrLzC8=Q#ct!W;}t$N(XE7MPn~a@07Sbd169r$ zGt@S*W2-~jnZe?~4LZpJqy(*Kikh>X+}6j>Du5C03I%B%*{~bYj`P_An&lpl026`{ z5)eSnNJMocQ{A0rC_;=?E42urrUd3}(s0H|+)ipFbP6~Z9~!Co1Sn!ID$EkHa*;3t zaFiht?h5FGTH`db#b8pK)D%T*yT@d{qk>e8#8lNxku;>ELs(j(x{-71EqhF2O;AXH zjIl+Dbi++IWqF|{21Z05d+S?glkwie2fq3{zkT)Kq3N_nVo~uK_!6p7RgWXYNqg_;F?mh-Id=@ao5a%De(1O1h=rI2afJ#yopLKlJ0RXd$T)26bNo|PE zcf2c11(BRD3@p?WGqqipH>7d>iNF2i_Ss2yv2R;j-PzP-&HxyY0oX88001TAI;=I5 zvYYSWxJLr)Qqdt2G#Sm>z#BW4oj-Gp(@Sr>W^r+u31hS%Ne%1JHZdZRV`4-!MH4g^ zjDjf{fEptxLI5>HGm2ny3r8{|#FCKF93mq%YEkCCJS0T`6vS3Ffg&R89^{ajI!lA6 z?|$giLr-Mgj;b1>0?=IQdI6~igekOFKli#@UUmE4&t7SR?!ddHPF{(QTQw745<@^# zRL4*{L@-9RxlBZb5YaBk4s9;IHBim6yjPTD;4lGY1R<=(F2Uo@3SGn4&?0fb4SMzLN~FMyXBEl9~xpGT1SkDk|bc zaa(j^Fi$qGAx70{ij3T0Tm@<-{`v#^*ABYgU_9Nf+mq+oLv(sDcp&G`bhA;%%~A>} z`CuJTWv6Ncs^{C-L|Q0lezT2WF)ov_jvb3i-T85 z%WbF~ps{x#N$0j`1ylghR5jXa0;O@SXItG6@n}B7GiT0?&z#2_%w%rD#LHHikq_NE^@#DpKvnqRvFcx(F#^TgI%;I%fxI->g-$3=q)-b_AW*gFV@9d^fi< zr)yfvmpZPV>8v9KKI5Y@UGnW_U) z5zIOD@`WtDRb7v6~L*18^>q$M#ELWDBMCbY%U zptrnWVi(0RrUtv$kpUr*v7>0<;$)i)%@Y~7&6Jtg=a}_SYTqpZ7)aRr*fuIn3_&}6 zM}AReha4`dEIGJ=rwfg>W)ZDJu(wEERx&Y^q}Bzfg*PIaApuk>QMfilfCw@%AY?C! zASpqC+yQsItu{cP+J?K%o!n{CZTnZ-m4zo3{i(X$?8f6goNTg2c94~hA@%sAxAG-6F*L;mJ7$Jgb2!m@5w`X_8 z51kks-jfe|a&AKwI=^mc!u!A>wGGT_$aCp>#9&*k#3p0fIX#X29Z^9f*-Qz55knhe z8(hxFEG8O|7<$1|+C^PBvQC5uG(~BkUP`63oV?E-Js6tvx-=!Yfa0oyAk4>%*|*?NZG;8OcFs>1_g?;@Fa1)RA6fuG10WU4d*Akle|YfF!B>6hS6_eo9Y6M8 ze&r{=^Sj#dwCoQ-P{8-lqEi>D6m?P+s7!T5!^|x#pFaMnpZk~J{jYxZ=U@H$ufFuU z=RS7VyVPPdb&g*2@|QjI@MBFo5{+~JQw;HWFSz63M;^-;qT&>c9hpFi(Pb1)RcU@CYlSk*dX#2rN~Ua} z1Zs>VD03pef`S?mfD=*>DFBT5&e6`{XfSOelTV>d^^8&u z4gfhjgqhiA7e~9 zvsp<`EN?d_`g?ZMP|;}8qzoiOQ>4X39?zs1vO`G8CYA5+?RGkuCfj8oFt<%`fZ5W% z-~O+^_Rhb0+xPwFpLxaSem?)2`85l}2iLAT zdg)|R6QhtgbBcz5yXs^3$JtyLWXNVVKlB1906sgG3z0NRTWxoTdwt$@MG@=i#p@T4 zLa1Upb6Lk1T|o0i56QLlD8-i0`(bDH=&@rz_s5;bPIo2lx$QZKc0+5at0;{h`9UoM9zSwpkPM8N~s6z0jJ3t!(^H>cvC_oglMW@1}qZ$ z&L>b1M#7YmA9f#j@5j5#3$Om}ua9+;+K9Xh2t}Y>xiS)h%_ob~tXG-n62fQqM3IkLr|4E4G*j=Uw%lzz~F*J91NamyP5$TEcf~v)maoXGA3ka&RJCR zQ3XIiO}H&AHQFg|=lF$t+`h~YDT0MIfiNUAG-O~0$rMAtD&nZ3NuoIqKVw~Fai*XN zX)ZEXL}(M%qAg-4!0~t!+K?L(qnRnPh|I~>L$rNP z*ZE%O78^~r>mp?2K?m4dGV3uqqUdp&P{n3q=c48Ft?J@cfvN(&-|JqvuYKf998JBM zE6daxX&{GKI%yLbai_3qhMDJC(vKgry#u^*FC(hK?m`4JaxN^Cuw6L>Krw=tq7TY0 z6GF;7%nk7|AtMvUoZGt6?UoAX9ZHt8<@JmfGC%-pB~7PFCW=0F$~1bACqB{4aoZOiQ2DCRKLQ)7~U^JO3p|&X@U;-M=bYa2A zq~&nICTHu-Gri>_l4gith;5rv+vMzo{PBnH`MDqZ;eYpQzwy$~eU1I>Pyeg${O)ms z2bMckpt2}3XHwO(1~U|yO=er&tjM>PK(&Mm7tn5oje^bJ zT%;%NJ~mUZwweF{xr{hh8#B1wvScZ-u-E%JO1hW=%c86uDof&!Uz8&415E zlYF@=3f;>t)et7fo>)4x4^0&?2q;+0xJa5(N^=D)l7Y1bjR^pVL9G%o26TYBscpwQ zL|91~y1a<-72W38r7KUKjoasD1t_6IbY=piXbFrF%)~k?huZq2g%kkUAXBSB@;NgS z4R!b1MWBe{m`HRY+IKEP%V7TX@-9LK4T%~RqPfd@ zzzg_NG7|$ZT3G5YEDs3VfLuV~z`(>qtiU1!fS~5cVw}}+DYGvfhMPJxWS9X=lp+G4 zVli==2&BmkcbHxp`#>X%-CmqJjC~VD3L*SHy#No1(S#|fC`EsM9_re2B!ft z_RG%c{D;1KFu-{khutFcp1p6U4KQ%-kV)raCozoCOc0zUv$Eh>pvTVHPHoPc8C{S` zm>Mu)k*BIb00K%G0~kb+1QZh)1Aq!tvsOefbAqB|$)t_5gfL>KV$GzfG#~*+N|RJU z(?IJDa|BM1T1+6(tZ|?o6>z@mDX;^ctEE6fsZ2&8h0Ro*OZE14R*ebC#B4H+W=0uK zTSGK!lLH8*MGp~;vGHl5l8n?0(TEUQO_UeTi&&3;PqfV?^ zW;bu}geZIW?dkRgF)9+xtJe#`CrG- zEQ%1p8c;Ntmy`2gDu+}G5deT~W1NC)SZYZUQ`yAlPG;9F=f1;JLlJ~|jFC zI~jN+jLL=gcizxH*TX?VLt`}%Lo`r9GZP?SW->%oOEHOviXfmW01>mBv#U~o7$wV? z=U!_=!?`(()MY85w!mPh5o0hwWdd_Lw*Vn%UP!0VoFxZrQBnpb6eR(65#G|s-z@os zp_?}cioVM`E-$FeoM#6Bxx*gtfbr4}-QIPp9?5{2eUT#r*j&1R-J^T)@CqP1#c*z> zZ42zNRXR83A{Xa$G(|OJ#@L$k$Q`)*xNPiT;Z;p-H+aBI-D2t6nA$cug^(mtOes_& zn6zpM08A6Mv0bmDwE*T5mH^bavuzU+x*912KL;N&%n&UB3f4`SwCXd-%vB?ffdt${ zC13ymW2l`rV9snI;$mXqwy)S##)P=Z=UvDy>zN?|rp#ulDM6NVo4}kSl+8`uu0W$z zO{${Amq7C>KZ*hzL0U;{xnyx6-s-MFbc;dl?uZw)I$538Cl=E~GbH zaIAraW@3oW7kSys%042-*$9WdaBl1T-+iz%=mbesQ`!DK`O3=V#MA5B=gs>FhJYEe zBg!LELZU>=G`xY-UoM zRiKG5nR(`t7!jf-G7>N*0>DPa4052N$VbOdzxP-F;g{EF}S$`^m(3y<7**^RGz`K@2^x)*)NS6%t?+e6#3n!g}-Hn5Am3A?a+ zCQC)`UwHHN1Vye$lzB7L*q|#^gGj<8ajH!$zG5F7^-b$IBX)#LI=8koEp3nD)(%W( zn0wv3q^nD|G_+2JfL6D*wQcL$aCU<06{N^R2TOTc?X-uw>8eZ@hj{X`{IMZ$tBT0k zTot6`m_6^R377z3K3Bh}zKJ1R04e|gsbHvsM75wcY8A*lk*J9(A{uZoG*vJ~G-e|q znM)%DAekz*5rTkvQc4I)h-{ECa)yYk`UkW4j@^4Hu?O}>1U`p6$4&>Cu;(qqz!rRH zU2KWq{K>PA-+6yjqaw%5K}K3)e&eBL=%fTxl+)92@{FN?nN>Biv#g^ch6Z8=Y8i+^ z{N!VHVgt*}zzh@-PyiP@xVDU~rcs64EuI@|)jIFNJ4prr#J(uI3NkvrK|q{2@Ghdr zP8+L9g^_9}GgT8MsH2TrZG+VyTN)Lip8eu$}DMW1%rn#{*bg3|R zW~X`*&+U25o>R;FRMD3>+}G_b<;;-U)-kxA%S*2SliCvEtd5!}D|%*$kO2@h)sqQW zs1;1hwr=w>@38*Zcm9iyyz`y=4j%fvFaPrI_|fkOJDZl3qS`hoM$@W|F{xNKnuey@ z-l+@f?7z&P+md_Uap#GLJ_HC$`>(n5x>thBo^$KXqiL%e=mI9z)P}<$BI#Tr`Rwd` zG&4&gFxTgyim0mYUPWpE{|K&ayQe*h%_N1|46yfG=ao$~aSyx2c zc9s|Ui9dUHc4o_W^48$YY($q|fNis!c znj_7`LlQJGaflgdZiiYDJSNs)-pA$z0I|pr&f3pk%g8L`cyvm`xfY@Uq%FGjz|%e zP}L-9KJ3(6+wc6(zdM`GT;^0R209wRGrthJaGXvdxo&ysb8fifOFsYk-}J_7Klde< zzv#B%zC9*J0EVPQbA8b>pf^JR+r<#XG$_j#-*CO7m|SNFq!@*~b6J@TNsPCV~41!swASR)i#I09%CdhWip)mt%V3IMFb;IQ$N2FOoBj`F+;QmXS>OJq^N>Sc7eOW z020AEiz`eS#my3rqD*WJn#pXx>p{We&W#upL~|3rj}_1 zgA!t-#ON4GuaY2lsHCP&2$I>Sj;SCWUQ|#BiXxbM=zE+P0mn9hfka6=jqNm$=4vjJ zXl}Wqr_=V?QFl-#&!Sw;WGf?f(PN%lX#k`E(GVz-s*#b7RJwp|8K)*NLz_{WU-^o$ zV_H}xEklwNSc}5)9h2$mw0~8WU3LA=C{vXQ_8wY!fz*>M+Z^;No{Zb7At3@AO=4%k zHxWUbjWef*d-j_uGOLjgnG>=e5@1%gAug<}N;UfBfAg~l?xyG6`ueZ=+NP?0?%)3V zrIjUiFq>BBy|ih;QPrjt#}4-FtT)$QGNdd#c-O|;{_t&I@h=zpt2g(T50Z1!ot-FB z*MVp*Xt*$arTL@Xa4iHdLp61ddi~Cv7nfzeKP+Q}7=?@o=bwdOhK6XnH534)*rr%z zMYkyWh|b^{d?q2sdZw}Atn3ZLl*~DHf=RVSohMkb*+L@8z{m_Nf@VrK-=+y$v;a0q^*+qkT$URx zhIoMF>XP2?<*Dd;YDi!PP1AN3`Y-yj*S`5@|9k0u$9XZW%#dhaO(V|BDa;I%*dsZn zC4f;%t$-(105S}ynW7SyX*SQo2F{R7K>!rY2kj3B=N~@yfnWQR&;Q|XuTlkSgz)TT zyZ^2MfH|}jn-CEQ5G2$WdJAN*JI8y*D`*!X2fO;qkkX(i2L(72P?-Vu9V@wk0h2{A zX`u7U6@9~E<;+4dM3}!Yqw8gb0fG{V8mO?D30PA@h{k}+Jw!Glo7+MZD5=%~#W)%| z({3gwL?jq^Lx3P5id6R1FPzZ^W?*8VsGG zvfy0$s79dV0EuFptLcH!ly%;i5n)N{oMw)q%(K@i34ob*FA>UcCbas&+4R+e9N+w(U+{*l@J$FK=XE7g|1tq zb|zIs6Rq1_U7=Zw%~n0@tQ189qwQI60D$bN907PsS{Wt;63`7-m5I$Tm?=QbwUP4z zl(*>eY6nu1`JrYu3H@0ba&KznEGoiQ)}CtY%GJ`mG9%K(7{6@k5Jj3ELfN!CacFRVEK_@fUIW5L<}HtKD28L|4LNQD2Yl0L$E}> za^>)qFl`XjG^IN1b|ea-wIwRM-0iq-nfE%GK=a5+oXlLO2kNO&G!!vyqt!;}Y24_j za&5$<5R4j8Vgibp*ArX%p@(f+p=XTd4=!Y@t7&_x6wrew?lMD2(Gmbq0%*+6&89mI zF|}r!I1RO`*G>+N;tXsANzD{hgH|a{F==dPjUsDHjkegEIHtH&%#zGc*p?D*Ph%wU z!8g%QRqDnax*=SB6?caFDRX)0m?(vK_S|}1jYB=%8ck+d_uR_Sll!jSSUX&H5e)l1 zm$_nL|6!!Ly^FLCG_*4XVDbbA(rkR> z?rhosVlX^yVH4uw6<72R9YI9d*zyc+*Q^SGEUE}Xrd%Qo^D;}&MfPd~qb1G&KoFE+ zpl+@8$xxztSKrOOmlv5m$UQ{pUp-s@k! zc*&uiNrUXjFsV#6GC`MI=GX`nLCm5hGH?hBj&g5`7KNs&W0RnoM3AY{II+~AsR1L3 zX^Av&85y)9uGjs7+O{qSW+@#ltgU_e6QB9aM?VSxFMjRked#xT?PS*Ogw!TyCIw5&SperS z&YF7bOd4&*n=TvfY>w~0_qfG#p&21Sckuv%nO%%2+>Q9$O)^u>dzrQIV}JMHU;qB6 z?tS2C?at*Cu-xr*i|o!vPQ3Ts_rLj1KJnJSy=!x0)?X^;`GoTt^g`SKrPxSp5sAs0 zssY*sp`WRy*r>?7?2-`2C(ijiXCgHNGl$&Ux8HX}nbm zd42~lo4YDo)r_e?yytJ;bN1n5zT2@R^EvrN%_QPK3fVwIvkS$mfx-MSpxHB^W_#uX zyZ-|iz^>;7f^KA66{{8rZnGd3_y(($f!+Z0!bl>u>vS4n0J~xR6#XC zCL(rbIROACfn;F2{Zxk#TKTo9-p4eYfA@-EV^TLG?scI_kU$G(Xof_Yr<^QDEHdBA zwP0|NBl_V0L}gNOzenhu&$P^><4xI8qKs{)Hpr~(TJA}}&gYA7o0gWBvS}$Mp zd0~0qgZGro)q|?ZQf6wrVGDNHAuvt~3NPbNF(k!Cgc zDAsDH2ie|yD6{Z`-}D{t|ND3DJ$&?Q|K)f6#4rA$MXFK@&5lmbjY1l?;?>%S=TE2g zCz}opU;6y3-u$N@{n$G{b>@jrrPwa49UZP5j;TSqDEme*m|wC*w^)DZ+^_z?-~Rg_ z{o6ly(}yb)B0C=~k zB5r3}&13@+^Ulyr4R?#I`8C(hwo)^hU%gv*-xu$Hvb)d&gbH*h%-#=P{*r8EW#`E! zM`zDwMG-aa&Pxozu~TzsiZGVo3B6*6vy9j&06=E6EWQ9HMs{dqIiN8{Gz{QXnZR<^ z3=>-I>@sv^|p^E^YE=NAGx1M=j^QjjFR%)pY0&A)kej>t4A7-=#96OojAOfjXzrHhjm z904H_f+V0h50;eBnJF8RQy@$c=2?#nYz%4!YPG8OSQW#28pIGA=W0)F7%am~)#noFnh`X?xO)##yH% z6GQ}8pz}E0q>OE`4{hMcLTCjcWtPaO0mncrr#IuLQ~sQ*+{!Ta3Sq!m3Y)dggq3}# zKaX-l!ZwJ{2t2f{28cwio8fwhGZV@*E8V;zo5kduxk4v0n_x864j?7N$SE^b@GwU& zSTGSaQnXPnnzfM*+|Vv8;?{^vWL!-TE*@rLF{?+7_ib5rOho~-YRV$t+1wiT3)yq< zA<@@A?>U!TdU!M)6A}WFQcPaB5JJRO#U~?^hA10gv;+iMKVOKW$xnUzze>OQ^S8hJ zC13RQUzwBrGcJXCR?W7S0E*6{A)%Rp0@C~{4fQm%v!c5Q24>*p)NlqPm>~DYL2Rz!(8)(?!ZA zO#>m)g&m?sZ7e4BYMK*<96{A51h(KYp+sd<=_YP7bp%Sb0TRhBmrgP{dg`Q)aH@Bu zh#~WIG8?WezV5rf?oWQ?7gi~A2!z&SZz_G@9zrrv!x_-jah*ezBpURUlfp(yPLat1 zqE{^(?n(;HB4TC;2DK$;RHC1vHLCsPrOElt_x;Kryy3^bmmQnx#ewbxQ4u1-|MlgN zP{@%nDQ2G1tO+T(JbUFrEFdNJd8y3=XCp-is1(f`Ht8tRBhgqRx19I>%7$|P9fIcF$6P9xEL90g`7fPf69W(tNtfSwePMbse@QkyK! z_d)^%(*&8vBoIuQfC$X=!eA)@lEL{PKknd_9={;jy5Mwky0{`JX0gRK0N}hUfH3#u z5F~Y~;23%(gg}Ju;NJGBQ=|2b!M;_C5iuaJI<`i!o{%RA2AGK+R&0?B z5~heL&4`N7_pvpzM68m<0uvU|pRT!0yG`O4VFYP8xg{-IfwP+Ys2z?&mwet0wz)l? ztcyyX=e^~%dV5l5IUN|9#A>@K4>*bKs8y$IYlj{TwlyB*-isI_7KQgkKB?=W?-=z$ z0OgvA(XvcABoQ=@jn6s>+<*D!KYYu}pZDFr{L`QJwO@7i+|KX+(yxORxrazn?d)_1 z8Kk0)Sf|(-WsBYN1vgy&*vC&?dikA2Z+~~NcX{6x8z&z!+x3{K88K-U{^q}b?5^`? zC%uVD?18(Cj$Bn0{>(j@XAwcfj^SyI-PM)h{=;Ur{^&!EwfoC{ z-yE7LSQlxZ`YB}%#7wa;H(?!FA^;%tyVEuqVXKq_ITtZ9)D|^b3{XKzGpxaSP70=3 zY^v7CbK$WxQjJgW>{{uhh`q^VkE;dQ6n){ttl7UYdH%wZr9gy9lf)=VQm$v5q8bo$ zf=XCA|F{0^ z72omIb8OlLz878)KEnU?osR&GKr}`I$xN%ZEjqm)zit25^_tmOCSz$Q>M|P)(3z=( zw$jmNhQN#ax`2cdt!nF8JU>E6v|QM7-+TeYYz!JxnxY2WTjD|w!6ism2dI=Xvnx_v zCNRngP!k)71d)USZZ1v~(G*15&}>aa3=|O5IZ`u5a%qkZVik1GX_DB85t>6q0y1ND zXvl~*ANMd37&~Q$Cg=z=5(V{+dJZyg3~Y+6TGgnsI>H!D1u{oKyOL9euBry+JKhnd5FIICyBWrQhGmkfYJx&ABg@hE8%;nN=JLL`{O!;G ztN)-$zUlkE^9^6~<&Qn_cvVkR2;(+Y3C0>{NNt^F8?)0JO@>WJv&ZjzdbIJ7sx0lj zVrBnTDgoyS9tPzwzxREQeefes%(}HtaL}*3pm)XR4zJ6_MGNx;^h(l++1YdP93qyO zCB;!Q>kd0_dB;6J{lkB@y)h+3&|MXC+g2&Hc`dyJZX!kOAj%L1#pS+n8!i4aS@h%vw_bh-e|2D$Ls= zqZD_x^~_G%Xhl8k>t-+Q3}5dTKL>}On=QYtSiXtMBbuco=R@3}v}w_ZnBC!VrAb^j zuO52x@t^sfyMF3-v(OS*OlpbPP>45Cy2*i|#pIR9uuVy4HBKi-g5BI5?&)s>%Kng?~23&W57`9FL%V5sc(Bng@+;;ux<@c)*>-kkhQ zl%b`1N3ycp`?4$dT%Fakn!r&CP}hQF&TEJy>cybr)55aLy1CX~; z*c6bEn8cDKL!0|>5=2F2AV4)GCQWFj>;QS**i=STLesfJPnb!RRh1dYArOG6w4jQD zieMhmJ46EZ2D;l@Ug zDwqK%Kr&2l@bf<>J9HE+fH7v?9B>i@5@hti0%FFV)r`DL)A5=2y*F)*Vn}(}@d`~P z0Klcslr+jDr~&~%n{;~?NnKV%6O1hf3DzM}RRUDkCpTcnhGymflDbxsnWhYft&7TL z>W}~eGdS9%(tF}kd`uKc3N#0H9$K04O&p>NUr8ZLLgSFNB zKK6+_KlaJzy!iPq`-0E;=zBjr**vH09BAE$PgUaz)Q&~LJZ?=f^KP=fQ%y&jqVt{J z!v4)OkIo^{-V>-k_2BwUTCrGian)eoVd|WoOq#sQ^Xgv33mAZyibXXe!d7FAk#pXf z%)*7FeKa#9Q_+;XFVJyHkjERT@ z$bg9z3Bf9h-MSi<#rdobAK#&;LH%9O|c>BGqe)bT$WKKn#AWmg%m^LJhNnI1W8~{0Lwgw7Fwl#37KG%G$&Pu ztO!PmRyr^MGe}Ayf|^M(RB(i176ftxMk{hqavDK`C67Hv$v}ZSynEIY0P+B2&h-pF zs&+-ceEn>;e)ngxo1Z^eToQKkOBON*LBIzf1W1qokj2BilVoP>ianqATHhbS|MD8$ z|8Vp4xnj8wS>dYIYn?ivjb#~ACr>*=7jgg)k?qN>+8UL>TvE4RILNFD(m12oiZv8^ zE)r5uJqyhq-_3#B)U?i)(f2W{oWuyuV`rNCi1RTgs}rFZxdWCvZDJa(?!D&vD-K=K z6nWM=ck0RMDK!9!LL=^Cwtp~w`h54`qPBH4ae%05#Y(=jxxQ4c+(Nb}^q{lIWD%UIVG1a~(DChPR0wxw?bZ){)4%+qcm2(+FL>T>zwJ+d z;`{&YJOB1QhxYD2w>@6#P_M8o!&bQ+&r+1(5I4`o;}1T$@8Frv(k0#D-m<@1jZPyM z=y2<)?N5I2@v5j}lG_*e-rC)BayCAvX|3CvkMJ68D{V6>DrsvB;*f!ibw`^X_=+T% z;jW|tn8-v#+Pv&#MK9JfBZu>_NK2Yhj57ex@z|=$&?nFtCsjuZtJmLz0C8u#KK?Xk zIW#lGIT-`ctb?q?H3a87m-@~VVfKX7ccUIH%E9BuHy(Jrdg}B&Z+))}yP+`Fl#3I#z)FCbnFE}e z6v4wBYx7a8lc~^F0 zVw*LTqOo_2E4eQ%jVnn?nM`NY%xr0>*+L%b*3>+JR?t@FnTRd#IAS&t1=vZp6?Ksr zfrOxnavRViDNSeOJxDDq1R0vLgaBo3!6XPSJD2-3OU@N8hgc_TKrEyRm?6cv_8qxj zXw{~c^8Fc{u1w95iUL?No14)PEGZZeL53h^jKly*z&uJa6);97a}GnoB+x|T45EO{ zrDX^NR<}lZQX#aG;7z2x!oPU^B~anDjhI?C(9A)on~b418oRJKp#p#$cUG%nCZ-!SAhm?J){C?gkoNhHaj~pZ*5Vt zcf{7W1HAOH8YeQW>X-~G@N_dId>%&9ChmaZlzveSSMK1Z!$o6TnQ*j*>D zy!G*7aFCqqE$o|Zo$|SBCe_oQK0O{)f#fJJ9irj=o9Cl*#IedV8*tKsv=A(Sg1|#So#HjFIB4ZY42*z9zWIxP_Q}cCc;JQ}88Lwcmyl2e zJ!r#}q7YbQV$=~@A87{384zJt$Os3m9YGxo^AoDoG!QBP)QHh611N}QNtn?QCbMMX zjU%dp0-*!UJLRX}^tR1oC+_%$FCMHd$EqR;}^u6rt`N}F?Dy_RVNHK82c^iHGcMFZen(oO~@o&``FRfJB4 zsMrd%Eu^Gj-m_849A-oqJ*b*Ga;@m^7u<>Iz&&@nynn^oVNeBBK@rj1yg`t92DPFi zKnl5YrPI3A7y%Jd+P?cPIk#a!M^BuS5VL-V7cxggYT`kwIJoJCy`TSDFi=Xs1_5O@ zLm+SjQ9XLmq}s-m*ckvAyIAJcjvF}FVOg)!d6+`2b(>Oevh1^xAoEc`87L==-U@SV zw5c>I9N8!em@apyYelI%*S9TjHe)K3+C&4%x-~b)M*3Xd`3$Lw zgI-ySM1pSJ;OP^^X#M=++E!<{)?eJWp5?$gJ2yLh-??B?P=s4aAmT|`%s`dRMCST6oNLIEA?}XEn&~F2UXpj0(bP)M&=AUK#l1{ zV-Rh`03e8-s$^fkd`U5+qyRJ~JH;)U<-kt07BwOv%>fN;puPiWNHF(HpdIDR6e3$b zY5GR@Nlde_Kg*WbC$hOhRtzPkObI-xn1a-dMM5%bXpYiXFS_mBo zC8pSl9w^*$290JO9fKk&<{9PQ6*O3+asb02=H4*1&DKOCWJ~#M6307IW>PGcKmbXy z%n-1xMAg6{=41|06cJm6^QL zKgs;m1Ph0bO%yXmgE&!ZL=Dw5ro?RxVA$=to`cw+%yZO9>u8CfD9kDMGGcI(Brf~Y zJ>5@r+{S}<*G-E=DD1g(nKx}D_r#2)R|Kg8=B%y3R*Qmwj0jxjZtrS-$-dP)uEoVw z>}1?4VbI6ULb0?`tPHb-MVMpFgJC1!0eTs9fK-sZWkk+F<}@~<3INVqx0KR*a60wc z1wNGGQ3E&0Pn?#!>ghu?J%`O)P2J{oX<0_Fg4zDm>mU=*7}Hs6tt~sy8Bn&XlN#X& zp?vPa^3t_>wdXD$UUAjFloA12vOQ`HOcpMx9R2QPyal%;F@R+vZlGla*CVF}2O?oPp-Wz~=)( z37gI2RMh|+?k;Cp@pyH1(?(Cn&2w?5vZNO%0}4vOh(;zX0AQrRV2L!MBBI39Oj>4E z71WgC3;>FutPG}72aHyFY$e^kPxR7&&77r;`vM_ zF`d??W(lZ`$_B=hE}Ww`&@f~PAsIw-$hAXIA;tjiI^iS3{zPM=nhaH`GF4DB$dEi(Mns4<2MH@Q zrmcoK{k*q#`H2sH=E3)VG#?I3<|*vE%Mj4!kKq64A~aBw{XO4t9#z0v1VYRZ8Ippj z0)P=HqyTLTpyoQP$;P9!)B~3>Wf_8zfda-fx2{rFAk1NYYRotd;=vYk5HLsPkOl<` zQ)F76>Q#xG3{0?{)}!r7W5K)&P*0k16Iu(Pts0PHvMOmJ&PdvbE+ZFHCFDJj*c_$B zgfXcw<(Y`i;`F*}_Wz47e(eu@-4}h&7ryHAoO}7k&dVJhDN)%Pz=jNxQs%gH$Oals zfjSNVZ4y-p`z4eFpu3hpREWXUh{&Rf^W?#`G9v%*Qs?}5r#gQg5Dh^^(R(d22!aYi zM!Cm+iBTaV9v0~5zcJJ*NfV?E%-mfmLmNl{RSZO`pebR=^ZOqeLJ(*J5fULe;*??t z(PD^68IX3_j1DM66DMY-$)rLxa9v&YxmUN7bUh|8h-hPpt!fLVpq>ex7C;3;7d$D{ zCO~s$pT#5nOTY3t*L>-7ulcH5F8{ilZ~VsREL^#)Q4M#Y>W*nH2hFHcWEr3Wu*+Jbu>s&OKY4A zG6C0`LuVg;$2%Up=Y9aV<7F>kvbLGkp>dXwN^;cgrXUbPovI)aVwlBHAu+pL!BSF@ zMC?{vF$uF4LJhI9w#FbP7mX?NjSm40n0BL6RUs)Rg9HdErh3|#8JDFS^ffeyh>cAC z_QN~B_xH0u`iIG99vv~9ALLs(sR;a!*8oNeHR@DizPRA>98}t^Ee6Qu4$})UIs`EH zCK*UR4@d+>#K5YAUObVsmL0@Y{@$pob#6_udUkO|RB(f;t~(mk2`_mtTav(}Y9^9~YgOj?;J2{1yS@g$@UEG$qr z-(@+lgPbWvZPjuDWL)igFLAxOe^~lHtlamh+tmP6+e)T09`=ZmO+rG;JfNd_5^a>| zh2~y(CNed0P6hgxt|Eg~jW0c9W|KC}DotUR;6_Nwkkm_KoIy8-W{QYXFe_Q(oGAcE z)MnZyYZ0udxK#}nV)PS=zO*HFI}_v#jUlL-D&5s?l`o z>}OSbWp81!vz~&0;;>sDRQve!RO2FI0-%6mm}qyu!wArj%_(b$${=%xxM&KHlrXij zvpHM5{6PEkx$&p&pMK#G&=D+f4_6;ba!=m;%Su0V;h@Ini4ZRMuQqHf<=QJ zjC*drft}wu_SpFG$0_%qf@&AGy?I;|r~&~oYHR3}PP?{73nonx04N9t^S}wF^|py* zNDvrZv|!O^#niPmsCU$FamC7{vW_hvcLiHc3(8$$LR3*tU}UUxnfko6znbi%9zcho zN?_&`Ma?1R3YDMlL0Gfqii|k3=nN~8iKPu^gwwoDAF-hLUQ{`n_WLG+Q7zBVa~2e$%4|08P^)TcMMkEc z#X?1?B_Wa!6u>fLr5_nfO$ zLtW|qOAoyCWw)GIZ~M#Tfdhwcxb-=+&CNgjrQc-|1SoPxh1;qVC>vxLryce)?shu7 zuyF08pSl0<-}KjC_RU{&$17g`;Sb#j2GiIOsgPAk9A(Q3<#cAyeiA)XIydfLz+As+sA~+XFaGrdLAV^H?Vsvw(r=u{dPXCub z(|6z1j%H&@(-TiGU4LVK{pD>t-Rkq>v&nXpCgb+O2j2S^f7<)N2L+9hB7*>|VZL_R zRR(bUlOLU)JL~7(a7TI}i`V9%1kOQ>qA(+A%`5>h)*xFFcX`){3N(ok0eVI!I7~Ex z5SkVuJ>afEn@kW|lLeE1z7Svq{+j<5fsdOU@chzP(gcsBkIFNZpF*#GO}HVQZOm;+-E*w@Uk&>cJY;>&M({mr`}rz|a|NeK6CgU}i zk*V87aKJ@wl4pzxIbdmY7F$Iar!-B!Cdh=^**C zz-fkcW8eV{dcspORB=FNQYaOGf{dV;Fw7E0(nz{Pu%xW5UOz12Ufwvf82~(4nDcIubePS+aF?u?R0~8yOsKq@M?$XG z`Q)Gf!`7MeFaD-4Df``2w?H)Sei*{8dE?@Lfd6}EJ-*aaB`?vGkmdh7$wJbIZ z+|Q&xK+cev#e~4b6cE5@(rPz1M9c=6nra&zW0*naA?pHpG8H4#1ZQfQsjN9)2VKfJ zBY+&5z?H(j(o!3Goi4xdRq^mG&G~7)wGmX%FfTeyiWxgIlq2OtmL%@K`mIE*2ILr8{=fkxg#N%7N8_YEI;V z3!aay?m)%WYaswm-fQvcVXuGH0iy$DIzuEnudW6rp z>55m}c*S3Q`oXt7aE}af4XuG`plySrS6ucp|N7q?Jg{$RVIj{;X5LNVQI!nGCC?5hT;Aa!0ceu8YKC3Oz*Nplc;cQkmF~?8LG2s$IggyCX|vD1=R_I{ObSUvbs!?3vRa`Ix1)ad}V7qPTfb zHXtJ*Ky2D%x=pre0zzBVY^t1S!vYZi)M(ov-qJ?U^C{izy4Sgq01E(bTtj+q9Dlg2 zFKg)tn>IG6T8M9Tn}cH_(mQ3)ehE*zTVnMth+UJ0UT zAS(>#JxDU^s8cnEaF)WJ3Et5yK054f>{Q8+00g)KtCQpafJO)&*$^k1G9X8!ri9dr zFcX^I`#0~>wte;Yd?PTJDH`oYIqe2^{x5ENKpNFj)%SkAs^4<#>?h7QSYvls4%dpT z@UHLxbI*TbGY2G+%q(L_$plmoO%2o}NnU#A4b)J;kTy#az`#V1gM)BB6u}MkF*u)9WOG-dC<{*A?zrrLoNEdAqb!+@+U?J#Hs0~v=N{aDu+!-Qvn7b_xEh}yZ=F1M{O*n8pLo+R|MK^K`M3P1 zANn^Z9(@9deVK=9X2~oWp$s}@?sG!*AZ-jHiJi33eAeBVwx>=#Rg|5pZoDxoJ1JV! z7^5_#$mEa_DU3!M0Zdd9GMgcnK3mMlS~bhfd36ZjjS6HYK$xGMF%!#0ET(B{XD9{J z^Sb#r?Opmor{4iO02L(g8H-r57NBAnZJa~I&Ve<*GPIZyasrzIh(IQ4265grWQ-=A zr3GIUaZ;7rv#xSWIx>ll40lhPJWn7(M8Iv8MzsKHg3gk*3Tr-}Pnt;)5H@17*W8PE zIE6T}I5w?8xAY`Nl z!pVsSQZm+L7|n|kLLt~ua!EzalpIqc=||TpG^m+EpK!6)9Uv;e{Oqg%igQg8W=3-= zK`;{l2I{$703s-;64PLH<>B{z;`HNB<=w7D0i5%z;37)uB7^FGufjEwHev2Fuj<=Niu+WytOPi(IjN*Wtx-SFv$&u?zuUC4*)iG}GL1zuH+TEbpYdP71a#;!$^t!Wcedc_r2 zKIh04YKo*5g%1qj_$aQ|ocQ#{&{5 zib1w_K(oot`ng3FnP`7FTga8?^XCndz*A}rC!J+j4W0IrKy)^ISn!t7|SyTj406^S5_2K;e z7Z`{PKyxHx6+`LDn=d=MJ{tA1qD^UaPyf)uLO0_WBQrnz(7g{nc-Q5J*9N(UrZF^0 zsnaX&c*)J@A3Ohv`|cgjY5-ea94Y~j8b}h$F^`}TK+y$YCe*sJmK}*F+uj@#(aP#_ z$y95|vcjP9$f;^+q1P#j6fsF~K35Y$XKJqSy#s^fBN}y>cL13)BWEU`eYAjQX zMGs%w>#nICis5hy@$^P5>73IRpbL;v+@e&NMg<`V=Qd4s@#v-H(Zkb^e`Eo&(*TPA zeTEe`k4c0CD&{gr`QiO$Hrv^l#=7)fZ!VzjKs*3*s2o<3W+p=-2iU3wjyo2BKny+8 z^4v9uw#i|nX}|A054z{9X%!G51K5TL>U+#@><#~Xy15lozbJsYpO^)aFf#xo#b#poBQGGbH+Dk3Hk331-vaXFX@ z7y@BTHfd$1+D7aG6^Y)x#Z zVR>Qr+#|O=I(i#BGy#|e%@{gNA{GJ^T?(y$<(*8M7KVj~X^g6#B4VNb5C87{pYxoX zZoBl-QB`NafQZi;`|Lt93FkcOc^YT2x_aZce8qKNI;|gn;&UE7{nhO0gQZ(n_9ig^!f7BBMI=#lw0d~u-1)6p07Nv^uv4Y= z3T%f97rm2+v2H~))B@3MZZ}1n5D+{o;)%R%A+A)88G?{gQnV_A3@r0VU28)(K--^1)jV&-;11aPA8NDY%ep3UeCrE*C=qX#QZHD8h{6*~A2|Ji6bp z|AL!uT0?glb`BrQT!;D=XbP01p1^{Om0H4<{zp-nF+ZY1`$2$^}0h@sB zrZjA0T<6rqsHSD!IdSaK(Z>0GS6_eWt#^Fj5C4ctjYf@O1jvD$0T|93qH)sfx#k7{ z*u4M#Su?|KmqmA2ToTz0`_e6oo0uGEFd+m4Xb~#VS&#*y{YA$B#*&_{o69@HJ30%d zIs%EN1`tW3nHcP&@(^d&XPu8sN6Uyvdd5}A&ZTGu1Rw}tC<;o* zU^xJpsY-*E>0AIe-&Y7gi*EO+kKO&wU;E=P{DE(;M-u}@=H1@gZUXrKK9R~fBmp%G zCE+!L%)3mIo5Nj*8PvkGCNP*EN7bfN$;?Q}=SBd*2-Fx=9h&ae*JpO(sa7X|%R~nU znM278HfuaT@oPO8bTLKop3q!p>*?;E)KKOWeCZ`YY(>Ek$$3VEwh5t)H!dE1Q#Y>? z=Z*m*GNSV|Agt>&YoRQ7R;4oMfs<(@7Icfa$p4s)D$K6PHN z=`NlcE@b9{5+E389RR>O#b-(6{X1^EB!uqp5F|s-Kk~I-_O&m+?a4D+r*<}4)haZ@ ze)pzBhp$@Qi%6qt(n>%8K~zF)5+7OVFolmi!HPX|3G}T`JpPvtJoK5TPwpKKe^}D; z!s0jHeoHxqnF7=`&B)QJ;JG=$>*7otcS& z8xEHzy#-V%e&&wi1keE9A`>W5r^wgh!a&>WG z9WsnkDg_ikosTmr!#gtgQLlLePf47jb}=V%xmhDtIa4q*7(r6QWB_XHeYL$Ye)8DD z+TPVmFW)oh&kPE}N+Bpr$z1M28#5wN0d~t*U4w|{9)38HCdLg76SeKO?#1dxQ-@fg zCa|e#4G9RhQk;aasbO2_GR|`W1XyJMx!K~HA2eiJ7{I7dKkRfysh)*)3ObR{DqZP| zPt=nU>5_1us6?P32#zqRvY-%EpgAEM=LTaMCX}a6j4cl zjdB1(>>wsm6!eN6$QRJnDrYbyO{!2bFPd${XogH^grYi8%YhKJn5PnnnZq0q2z0)= zD72RHrG>lS`o6U*E`8CLy`~;bRP0&$6$H@#-MKC)5pg1eTJbZdPBtNusA)nKZJSuv zriycSvsnrt(ejLu(acgxXhw)Y$b{sW2*x?S})Z?pqjH;60e1Ods*B0QjFQSSb zEqBHlHlrd{=)IVwI;OZwc~+BT5+tfx-NvJxwKuF>Qd&g>uH!Al6r#3*-5z(m&KfN` zI&Gv@B+~?tI}HX=g|yS{e*CV-KKb~uqR7P*4DceI`eJk(!2FHjE}caQF~&5l;%GLV z%qG=NNK-J3LOq{9|Jp0Q{6)|Ix);Cj+h6g@uYArOmoD!a)$LAIt0B%)jhrWTfTT@y z`-X7!-YPF2q4sC*`^-pj%0ZWxmQaDvUq5p zAxfJ@8{1YTF;j*#YQy~aNN+8`fbAXY@ z({?AN=!#xR9a3vlHX??=BaLE^rg7<-Lu-eZV=d&I0jP?q&~6cdySOvJi#-%GV4owR zNdizp2q_&{8h+DDU$lUkw2k+wfNfjHFaZO~3cC<4I6uzp=8mb=TjLkK_SV_XWVEq! z!}D+3d-za2o931lJ%ktr<(g79YikDeg_IC;bWYV}!?k?}MCGZcPi~GTQ4`>-iZTg# ztY=}mGmUXPvGL}VICJb(MNJ{8HSy9nKCgFl*lnhl882q8aEcfsH1lWUiq|hMq%d9A zRJd%}=ai(ew60;6L78XsYc4m+6}-)LWH_77PCfL1ne~qxSzg|=kQBijCPF=z6p#=s zp}80r7MAv0ewC)Q^~giyJI)A@me`jC=EP`h2FKd=32TmPy&=s;3S-kv6*Be?&7{?2 z0DW+a>;mh=(nwQcH_Uw7Hf>5wM1U=*Gwd4WKy6G#0u00nJt8P1FedUu2|y88LCuih zoQfLc2#q)N#!RSWM3Df9J)i+N!6os7L|wrp^qFB};*@gZ%y1w0!r);G52oahh%s8~ z5_KFCYEmUIPk>-bkO1dKC`Ah*wTb`=pc*Km0VLCIr}y#y_NNbg=rd-!q3ucrXb5T- zGj+|t>|&rUJZq7es2C%(kv{a)XnR(b4uSxPq&lWH5)nC`2e8Lvir#0Ik^rKlWF|~t zgvex&3{)744he!OjVqo7RnJe(U{4R$hS4OTMqJ#d`!1K&OL1W#qB$UCN)kX7iIOTB zps1N8gkX`We3dM|;OtH>j3yKTjDu(t5Ws~TFK-T?DjV)OjNy8KYri^CdWT5AQt zI5G&DkC3+WYCE4c95ptYQm4}$`kqRHryIFqgRnf@Zui9*q#0h((gB5IJem!e#po-*K5$lOa-pKpvE$SRw-eIEZ!t zFaUY8(q(<$ZOFDU0YKF@+tpMJfneYZ0V@OwrP_frtTxXBKxc6=>kRfG9q4pg1u@!) zVNy5qMz(^s-ebQPs!7_~9&(vX7nPp_gBNlCa_=1r89v^m4^6B4Vs)%(&xA&iRKWlo zVFs|%wg%9kZn2I~qL??%gQ^5oX-yR^BV;5XM6d=d8Al^l8YmM}L=b?ka!wpbYn6zG zAb`4)Bqiwv#sJ1BU>Z$H#gUK#nRDb68K}d)L%s$r4B1c#fU#%318`E~X%i0)3>e9P z6m-bkacqS1KrmuTfK3upn1`f!a><}c(Gce60zxFU%sUD3BY*VfNA7wc>vc_Zmk)oj zA%|x3_go;60z8u{PK2mYEGB|5iTHa@SEHbdXl<*2?AQZ?kukfrwe6WiRFZi@w-NHZ zxo0xZ$|V!tn5OkfObL=G=OP{+8m0X`92Ou6W$$J2rQZ;q_vLuw7r=AgsJDM@He7*} z9IBuX0KsHlg+>r$W`HDNXF4e_d*PS9`k#I0jjy|D^&ocr%&OVe&TL}`BD&=cVNg?G zX9?ysI7Y#`A#x;$-rx1;6AwOpqR6~nNNqN}kRyj+7h{$H6k!);Hov^*C*ma1OwE9O zi3E)3n2Fc{&!0(zyLFwVBq~gtxlWe*g~iUPbLW5gcmC?P-txZt9(cMS&dQ7mpHdpG zE_FIx2@QDVjORfr#01_GF@sA6&T+oH;#Zc;%&OTk5O#|v+v(|c5)m@bj$<>cT5Ti7 zvg@SMW@no9JHty3mc~d)BS*^=A@Bsu91x0jxlBzx-P$aM-QJ)?6kqT%bs4EN57F;H z8%w~s>NxM3nW?D63q|d`yxqOcUPv%7=L$1RsYOFV0#gPEG4_kjtDkf0>cV1*00wQn zEvZJ%03C?JZeWTDDxe{t0I>6y-?0DTJMRX7+h6~xrK5+oVv6VxF~r6z2W#q3)lGDm zcbGWV2M@3AzxJ}Nv*#c9@TV!o(zhvJR_=O_k%~G=OBmR;HX9XSpJ|GqreF#dtM;ld zdj5ec_uFW^Y;GXbVfLOB0RV=CIirGQoJlxPq#}@@-rpQFd#@g0a)s(XV8aNaMF_46907<5z2AE81O+iiA%)?en zPfz7g2M=~3IcpY{rnh|2tUP+=Tzmf9Y-cNNY&nO%*Uyp3v?i1kE3g20;V5Iygg_Fu zRRHMr_TF^ekym`*jbHO0UiwX6^Ssx+;PRXIX1(V0wpd1;3`mU#5>mzhYRKf#IW?EO z=zR}9@$q{f%AGTqM*&`pkGptBfQwmk7v8BCBj_|W&2$?Pv$Bs!^VnIuV2(9|WF`qz zKvM*Z08k8;Hpa8x`qQ_6`}h3f2j2PV@y?{KE1&tyc}>#R)oWhzVoz9|I$f`iEG0F= zETe$b=|j;omy^o`0M{1sL;CHkCbT8$wPu>66!KKnWgg^ zS+BeAn#(QAob#nnw*g0}z@1cxKm*idFx{Sv?t23Be0c5U(nfNQ*pTL}MT`4}t5@uY z6p4M&TLOUCPGhWrXt%Xz&#G>5KF}av4iPa_6T8p|gbViJUQsa4C+1d-&c}9^7hRWk z&2-mPx%(@a*Ob&W&2wIS&4VBP*km+$<2QcgH-F!^rfLd~jKKphW@Rf`9YC0h4wGtZ z<%_@l8@m1e$KLsYyFYo4w;s8U#fqdxh$x9f3C+c3bZWbr<%k&DsaixrLX~c@F*{8#-D^LJx+w8gG^1Rbsf8?RCvpvjreaT~Qo zoChBq>Gr>(xAaBj!t=5|GM<}-CpV~N0EfQuQG z4AIok90L>1)TU~gab`pYfS5$*ao_@GI8Pw~=luBC$+!R7A0e|Nr}MzN3-UdH3vbLW z<_*ms6Cju&s3EIrHO%l>oBf-ItKUCa&0_2!bdjQ}#CWQ*slsAGfYi;fi`oM$QW^jb zJ@nZ69+)9{6pu5}X+Vc~wB*GS4HIT1?7vo{$7)8ZGL6(4M+r^rcY8UaoO=My++*91 z^W;Ppw;eaGIhU%9M&qn*cDBbm=eO3+ug9?*ggsw);3ePpqObqKmw)|QzZbSX5O<~pG-b|?1?4@M{K*rKmMcqIGjIxCUVYfu6k5J6iKt`rl=I1 z0kSjLooy?cDi~PqxYzN+#Xe9NZ=OQLvbX%-fB${|>8F1IYCx60XSz8pirjL}i=ta} zqAB|9ji3AK*T3%dfH-~l0Sk?!N@Im78Iqs^Dj{IbS~%AoW(!M*IDP6kMB+iQI_Mni zcGvO_f}J^EZP%&0=ogksHQt(ywjO^H5r>EOtt>5e6uTLRJSrs;h(4BuF9Zz>uCinA z|2P6%`(>|Z3AVL^8G;L*p(J-7rif4ozG;xF@Eqx4-Fc z%d-63ulka&`JQh-fAVxwHOWFUL6@txaXi~NvpsF>OaH~Ue8D$9A-^_ZTRY+`sy$G#jkqZ4}b0pzu?yE z4(|5~01!YMFe7LH&S9E4D2NjSnU%;G0SrwIXLS#_R4$D>Mtky(AC1F!Y-R^8zjg>2 zCPTx$R9xx!u@vqN)nRbGpogxw*33>n@Zff83ig>oN&*NqnK%GaFnnS>I;PcHncbWh zU+kA(^}MTJ@HN-H=7*kl?N=TsZ{tV8LuOF-X!iO}@7wm?`16NW-q@c7jbw<{fNW9> zEM$}%Dx+h{2wQ;>g$;|HfQ2#<^?(+Q7mNa8FeS!^QfMTtpqt0~BS%GWisH-xM6b1% z4p0}Ubj~|&K}bmiNN~W82-Y!l0d9!J$0EI3-3My-!N&bVqmOHv=1>*6OeINga-~2P*FAxQ#EaV> zB9duimLLfcqjQRRnWia(-+N;G19$KI^dr?nJ5`^wt*L`{*|MWWBEYC zZ>jlrH}Kb{3;%Jl{F^)7N-+b=P>j`tRDpj#X7PO}kX|x(j6+#xQDuoa+J*S`~m0 zJp(cn-Wug1OJon6IB|M&EAw7=3tan0`Em1y7ydxo{n%W~VkY%;tF6abIVg$&sDR?! zEt5IlFY{3y?>TnrxBueZKk#F}_dVbDPrBl~$FrZf+f<9y<$YIPGDvaJXI*pV#2f(&bga`Y_l9h+ zP(Jm6cW*s;{OGG*eB1YYbFqJQy1rf_cM|H}-u17!*-=6+_8omLBF`r0MjIy>ouQf` zz{L}>U6}4_mKRyki=l3s9Y%MtRr<`64hR7NSz=r5oHc;{(n02VXx)Wm1e~{81r1d6 zg|EEzP54z~1pH?|_JcqAYd?A2O_!?6sup~h8u9+iFaO#f{%61cp}+mkAN$v$ z>1V(Hr=Ps%k%d7I*|R4AQ!GP5k!dB9MkH1B!K<#k?568gwV7-ZlI_NW0y4%rN{IVz zyzIKyKks$_;f-JV3t!P$=>dSwu_lH!>XbB)wU`FN1v&tmS(+bwn-C4K%$OKJphRD9 zZ?!v{0I+z)m1~`TVL-}VbvI=Hu}2z13Y$SLNpo*FLA*=)?_| zB!kjC8Wco}nJ<9$X3T`d)(7sg>kwy%PIaK%1IL7jRscQGRv&D~OW-nrz(7iQqztHI ztfnJpvjV5AfM^7br5Z#L6f6VRA?_I^6Ed74+?LS|y=*g>6=*%&NbaLM@{txEP(BXk z3s~@!6k<{VGc*R6ij9&61v6|x5COy-A|pjrheQa;pf~K_^XG5-_}{#{Gbk12l7rpB z*fa9Ie|+3?hy^1UGMJ~#NoZBXgAL34OcS3zU*9=up16vpS^hd-2$j1mMnv|mK0*gWrzsMqw{C~`4w0E;oiX~ zK6dW{ElZiPZ_P18q0&Q=n1BorDT}yqetcqm>yowA3g*}8vqe1@1$-B!x4T>3Zp5aN zNZU-#ce;y3Z?&3k0h+4zy5+ge(VO4($#;L`{%U(?^ZYnRYiFPN_5bo~U-h+L^*NvO z>Oc6)H~rX;{+lPCI?8Vp zAOJ`X7JNeNUUd~9Zr%6r=;0?4pb3z0G*cIMDq{i5pl+LxR9)NizB+v()~)Z9%ePtKA2s(YsrRO$JpFZ{nfAySy|Gl^W+wZ;dtG?hvKlGE+k3BgZ zZ+6!9U3&eRi}~8o+spn^HQjmop$};a%uY?wa9(wh?TYmXQC0KKG7;C4a}ryJxtZ?% zT<4#qI6uHDfOBOv*^p3mhI@Ru6zY*_=YzjbHuBFMGqOW2dX>tZipSw>)y$6~m6l0-5BQ!z5|I`^XKw$~ZNu&_usuHE=uJKJYZraJEr zZ8jmtZgH;%0_SjPuV31W&f(@s8NOmLIT2^;Eo}*SkVIaM>ey&v(1AK+&7&*K0hy9@ z1OZF|80V%~+!CurS4+_PKWOS{n z7=LBZy-q(!*6;VwLi9HAGHK)q1s3TLNpk`)C5tw?43Df{c6)v== zL;*~Qxl`sJ{GGo%dh0cZFFick*dfP&yO*wA)L5BatZ<#F0Ft3-qW~>}qOp+)VD7kN zs|DVBzGC(v2CWd1`n`*O)@nV@vdod0MpqWBLCLMMQUfI1&8g!R0CUGXNuQj+VlnkQ zXS8#eGe*Q|n9f4mrA{>)jknI^DfSUkwG%NhI&CVGRQq44k{rMlvf&XP}dVdib)OF1GHj{A_`_Iwo^6pWGwjK;;1kF#c;P{zk3{< zADR&f4N_>q%$KFAdPm*N?~G?}{otMNdjDN#PH%S5dIcjWX#VCu|I5XN!8d>Vx4!b# zuh`o7p7*}{JtuayOT~ki9Qx|7{PM&9W6L{z`o!b++_%sf21Z3A&k!MG2`oty(Wwfg zpxN>=BBt}}GOH9AqoEoDp}GR4)&ypXL}pQRVpvSaVLbL_F+8}x;4D~@gdOJ%tRp3* zqy{A-D7X1=^MjvmHa3fu<;BY`yXBvK^ND@$-8gKzXUj4F8w|B)2SHiPa49)iG^?N^h=ZD|??%^;4 zfSin_4VWvmrm@U9h>~-rG3)pDTyn_FVyKfuMtTOtyL*+)<7Y{tT1@jV+#${+P9&7< zbKqq2Xs{#}2jKeX=Z)Y3xdCiLOd^FRMe_z>XKZG!EVG3HZ*1mF5zHm%8m}N73vIP; zFL(M9S{=YjykGeRgrByULPEpBvk@t_v7mE} zd%8+{bJy{ZS|kNQ0{{jxglJ|+YGz>bBG&^V0K**HZ|t)$o89&PPnNyjpx0^YmL01q z&Tq4H!HjUB!ga=M$e=R-F+frUSE>d|0+PV6pk4o#_Zet(iwCH;5KrBU6wUXw8oA!0 z0xO9x`Z3m-@nQ)X8==d+ZbbXcXd;Q@nidkw+6DnrRocTOKiS=yO?<{pMzoH#a zPrT)Av-4ZAMVL&JX>GuT6&_QgDL`s@D$-`DpZO;rditpkfBgC{eZzt0-g@B5Bj-;% z-L(>e&8Q_%N_kVH0hvS_cKSOHAA8qV{PSDC|DP>hch#X+zUZ?r@T`?Wb?)Q?TW22e zF5A^W?W(#jxV^y)Nm6vq4_A+v>2&)n0Rign{`kz;$YAdNGets4aeMt(XSnuRq1trw zgqaY~yd|^il|c~EZohlwb(h@piATe<`H64(uB)H-g6F;DIR~!3c4^op!gE`j#~*p@ zfsft&=-qeM+-B=CsBXlASC2q}>oC|(r%BK94WOf%8oA&ZbehRKr(0yT4yDo7?ywtkW6x9kI(|3LJLZr;v>KJ*Z02Xqu>6s zKQvq%G3>0|{VVY{(s zb-#?a(`4)E)G|PB5GRwKW3MI$s2}Xf7+qPSLFC=Q0&>_}^A31tR$a4q@9+G} z@4oYiCw}C2{^0b+XkoFN+K^^|-jPK$0dWxR(c`D12t6xK{o;Q2?6M!9(X4>Yk11x% z*>rTSu11~C(j%v5zwpbyeeV2Zw6#@|iv}SiFcURFgAlvDu8RE9fBp6Me)wZAdj9jS zx$^48_clh?La8cRbf2s29lsW|S%lDL(WM??3#~I|gfeR=@b= zJHPnryW;a+qSag@F@p?VpB*$|vBjLQhz4)_4L9B3Q&%gMazX}nn@$~d#50qVRh5&l;TS2&x zGTrG+pFZBclw8T9gABUQ(HMo<7Vb92cK8mjim z1>(t__w3wzfA8So?#e2(ZPxE314Gxcu6R6B_HKUptGa{X#uJY(*YW&9x6wKPnt)^Q zgoQyubfVRO=*rGRpA07+1CaL6Wt~f}+_QLKeD6o1>*{o@62WKWv)t$0EFuD^$*G5F za}&1D1LX*AF8{T$;=@h6$`wkwV7>#C0Vrqz7?l$$0V!HgSatqz;U8+6tGxq9HOsbI z12!}!!${^Fim6wk?P&`UiRN+g40(atq6wGo*!srKq$;yqM2MbU+df;wIDd=JcJtZ%b0k*V zPj$LCx4QIMU~ezaI-PE+tB9~W#(5iYnquG`)Vc#mNBNrym~50nk;9^gc=Z6_Rf6f!4LezfBn2){IUIC^!k7J>%Z=t znl5D}*2Jn+QMH|{jcQe+5iG!_!;t};K9zp^&qJdtSL}P^FMQ>2kj1FvGM5$e`8EPv zjAtPOg}KUyMC!cgm?G$Mzh&VdcP8 zu^z`bBXR%$x~rfh#HNl%uReIiwMQO!;AzgFx4aO<>$fAlt>Gk3i1hI-rrA>y+$2T?q2e)67U z|M7PUCevjt-Ybqpf43^GA8mb$+@vB1zdWn>%p}q!x_K+&u49l#6nD z?6K_!?mO_gpF7-lumD0N05srbbth8&+^ddUbVO)%$y_LjiqVP; z6Kd#m�ZOcCPQ@LXN4%7|j5PwaaSC3Y0*dARwyEHJ&uLY%VV^p8wPXfA<@I@ulDM zb=9QOd3yS8rwR-~%rft}C#5^l=_UyekXIWlCJDY5rE|&H? z?_;RFS)Uk;7JXJgjIkAIkuwU-EW}yzA-0lCgSpbJUey`wbBKnf$j(&irdjUy-}u5i zo<4o%?uVc5^atwZ)Eq+~$hZ#8=RNP1JxdEA3Btv8C+;5T0H1a4!)JGV%q-7*-G*QL zZ+~&wwKw1KoZFs!_@QG@onU4aA;l!3W+Dm%d6q-J0Oz!AOxj-AE0z}eYis=?`|2-v z>GjuKTG!$j+BWRpw+sj$_~^rHOIe`y!^fZg_=)33j~+Q(3}&O9BVYQ(%hz4MdC#4v z-ugD{bOS1(&pKQpce=UHG7gY+o%LLtD$jg_5CxW?(`xhNJ@+4Z-YxsCy*jpf*WbP; zxuU7%bQaEmY!BQyZK`o=btXtsMB)Ona%s>5pgX?eg;%`n>UO5id%#`c3GG_^Bi+V^QR&j8UfC?KeF8vjJcSU>*>Z|`B2$i>Mb9b zY@gKF0`Uc49|D>tE4yXkWO{yUYp1nn8ZBu{%`}IiMct{#O;XPVC>8+8Wh&}qmSsb< zWZr=RC8=UOcwqm(|E>Sh9rpk9=YI7azxk(YmtJ<+O?xE@&Ewzas2DZQvy&hA%u|2( zmg$Le@xaRUU-1>?aPi#358nTGZ-Kl#RqZt1g0q0So*Fgm1Lr>9|5aa+4+iJ|^v|Z7 z8~uH2mc&wY>}rR!u~qi_2fp}?X>0TJTi&vjJBL}J7RkbNa`h`-b=?3 zeDt9^1`GQDJ>a<*5Ugn0#hY$A@bXu3;hsG8=8c_`1(pPO=z3gSLd4MN!kLrY>4rl` z4j$hB#_8|{2I?qS(ic2v`QXXCY;j}#dj7RH1$XM{~n zcc^W^BB~iCBoy2aZou^6rrw)#4lD)*QZQg8V~Z*bG&7rl1b_xr0CV6m#1W;Z_ygt~*pmBU`!ZeA9hL0)u9f(~Jhz!IIQab6aKX8UA4UKgY`#U=((0W()1!i5ey z2mv{EfRG3fAtp=FVgyA5BqpDE1z25K z(U<_{T1!yTD=t0g7^7(?_ltRXa%=NLXHUc!JKg^9^2-5W`pDygwELx?ArW;QI&?V~ zgCg(Y!XO{?T_>ZGeGfC|QPFdNb9dhd0Ece6Ht+e+j*CqD9tY$WD_$J?e##5xMa(@> zaH?v4E9RLSUbego)Kk4cg*TX=0OsZ2g;5v+iG;GZL@on>db(|r014+s&PDMZ0@}s@ z=YoLL0Er}oYP62TdC~6<_omb$T#7DG?ho0d|-fP>lgf z1q5UPWKuJ*)RF=>F=ul5aJp09aK}w+2lhv=r2p}WW6R5_vwGmI9NwJaH2L2sBLWQOx6egGSTtW#X) zG`2JWX;7ysN1ZQ#w^cK@o92^1g%t_JLr$C<=fv~mX%A!whS;EvC{?HpYEWxb0;mB1 znTrZWz({5$^JkhV=A7j|_dX*~?Pn<7pA39DVX>AC28@EUR$B!_WT4#%4$g1Qs%SbV zbRcwd|M2@hUDtK)GcZN4dH2a@S*I-Te&}v8BO|hUVwzM)Re*Bk3<-f4F*gPC?DidQ zo2@w7ArTWX0ccu?QPm+618X2N7ZM-O_mEdnX+Xh&FTE=qA`Gfyvh&DnXA?jZ8jF!6 zEcy$V-oUQYRMSP&Wx(#rp+SEka#;^n>L{I}-(NXQ2XF4J9$H+;7nTM^*N-$zb(Zx4 z66oAxW5<8`zy9T~|JL72(qH<;UzkiLuYbcEe(Gm_s@Lyrk9LSKq-3g| zx#;y>uTyYV0He#w|MFCa`sREcKqb=OP+h%YrgYaR?Q8g?$_bK@_ie$e_9SHLk3go#6dnyu5K1D zTlw4{d*hMYE{(GmdAEW^K!DwLKVG2C83H4VG`_dEdiVwcNTJ?1dEbTdZEo<}-3tIN zPATRe!`+X85!3k-_q6o{k@g+AwX<{}hK7)d7;v|62_fvg^vGvZA9HNQB;(wfBD;~Q z#Do9}0aMIO0tiYQP{0ixMy8A@_mrJKcjkF7e$IFP_>Ums@BiF?dGPN0i`B*RPaO+Y zVg^-H+#T?tBb&8bcRti^Ptb@3)9#HCya9t1g2U+c#A26IL~_Q3N3?F{nr+ccV7jpt z+rZ?a>0mejmrc%`3FA@kvP=EKV%`d>WvU&e8CmDDqlj3|W|Q@;{keZ{HTm7i_I4Ye zoApZY;5cAZP&Jy4?cibJp+XtK=;8b1{3-YJ!^P1{i=$U(vnrZ^FKD#o>KX=%ngmDZ zP=(4NE$>b1k5?WshIDUTH)sVC80QA&8d)^V(5Whc0Yiq!V5(`NA%M-J%C{*BL7@&Ar)rHUsw!!gC(enCD5F-$ z1k!ZYoZW0T0|a(T#K3Z-0x3iCMjBIZ z(A~WEk-z=rKgb4ytjL3j5tVr!wSDi$-~Y$|@Yh6H$6^B_fQYCzKM$gbDiI;6rjQ@H z3P5c0B*d}#%!s+yw-|A@>j$dT)l#a`-|lxloG0{C7Fy|r?)d&4^=%}IqG~*gO)_YUU6{W zU}1GIK2Oag0`HpPjdssyj(BG>{`}`X_jmuzzy6#%Zrt9UFr%R|BuMGur%xyVUTonZ z*lxSs(4O@X1r_af3dio(|KP3v_NRXTEpPqE=GIP`@h3n0(f{-x{}TY*c=Jua@!S86 zz(zZp{a&~1lx4TmU0&@B7m7Sb=g>1$6H#D;vh-j0x)=2`QZbxoM-qe(S5_Bpxc0JU z8da!`vaxmkl`nnStH0@+hu2;`+8KZJzx`>9sgg7kY?YI{fstytj6^waC5)tdQ_VLxO-ePyhsqG`47o<$9on-ot1-2u6|iw4nNz@JNCeP+Gaw| zf$6Tx@8Z^XVf-;aQ11@L!RCQ;p_!e2;uFK|L=d}XZP&e z|BgTX^SAuo@3{`l>W$InZ2SBUc{Tu2(|OV0T|B#ikDl69#*{Rf?kyZe6#FDUdlqOX`Y|fmW9(%mAXYcC4eXebDa*nu2vRW+c zz3Msui096Roh{09LVhB)zc)R9XEjDLjBx?noTj(1;vT%WMVHQJjeGketPEuy73UaMQM=!7CtAFlWHYoM9x(BNmAqa>JU)I?8?G zo^oakhRI9gESPL@42a{7kH|}%RC@NNzDz}7nFsCX#gYqAG*Me433OsCdbX9a`*-ZI zjH`Xw;?*6u$WumB3}!(A00`Ivdenjt=Y5{#!Lj##{I|aE=TAKR*3;l*}V#(QP~|=bnhXTm9``Nf(r{ZZw-R z>Q8?2SMFOLG%0ZDln|JnID4*XTV}pc-@--Cz7pbvqh4A1#g*<;r#JuYfBfCw{O@n6 zwnqh%nYK+c81#SVSAP9h|LdJORHNm7!QiYT_8n(bQh|{WSV{$$6nQ?|f6ijd?btAPUTU&VYL!bJ{ zPyJ`iv*ArwKmT`rrgO(F8^_PJwQNPH0HQ~k#mirQ{i}cci*Nek=P_}dwet=6LP7@F zMHl0RcbNo3=q~I%dfh9E?&5go)R|+SZYNtr?xN$z3=HS5xU19KEszcI0`6u$8TWa; zbMDFe-oAbAai4XLUh}fWy_b?{O3@7GJcihYtjw;v(elBrAW#@W%{gL{AIxBla|{>OiN@bb&v^;duYQ~&A*eeRKS<*cb?1;U(AkWN}!7kx4{57KlSG&K=TO^gjaLWNcm=BV4KUU_ekotz~f%_l%L z_GD;^5YPZYXjcI{b5^q{+R$8IxtFLfyeAe*!L2zuU>O<~W_eE2%2H@dQn%MxDYC)9 z7yYcv+_3MdpZU@oF?1P_JEaAKts3v&geM8)XxVvApN7x|ZAD{}q^dASd7~r=q0uI+ z6veUkfBGH2|F;i5_VBO&>2Lk%-~H8iG9~6b_YM?P6Bz-bLPBHH1eP3QicyiXm7_A= zLT#{I4Fke3l#M$SjeUmio6{xzCEuwO{##$N%6>0+wY+NI+x&O@!6< z_m*fvbT)L)9urz9{m^^xU3@< z5cmRA*`9?i1lWaBpZ1+f(%v`RTjGay}@4w{BUiGa%_Pu)!AFQX<`~K!FKlZPF ztc%W+$J3^sg&xYeC)Xhf5-1|gr??@xwZ*+JzC}ND*Y@^yNp7xq5Ce$GC!MIwYwsM< z$odHp*nyjWM_r`hAgeoA2f4SIW{G-0ORng(&7p@LT(vJ=yz&~56eSeoB)0ytL&csw zW_I#{haMZ(*n;+ z6*8n2?)e1gp7-vfLs!Yp8SqYPSUBMGJLS=LwG|*PD6FzO#6?HU6XnFGOrzOSBm`uk z93Y~a33|*8oq0jorgpERhk#BbIZ!xn&?01Fg;EVF0Hv`5n3;M(HkeQ%L`KMzyjgD4 zccn)w41sJCDQC_KCq7{0P@6bNjSdP8YNW6LrVDNww1ZgBbkmL#b)ZFCCB8A(#i zyk|(NfJRJneto`r)LhxFKLntN9we#H$YHXC%n)XnQ0lQ2F3$&w<#3oC+B>Yr<2}#0 z4U(Sy`@iSegccTnu$f6)rKM%Q79O3nE3w(*Akad=%-8@%aL$OGL`8{?9OmB0z@lez z?4zWV1m>L>Oi9TEoin>Am<62&Ws@p^DiS-MRP7f$_txKj>-!#f`sA>ab48}8CIEE7 zmIh>~B#dZ6VrgY@;ZyfM@#DYzXQxh_?>jdbc2ZSyQIMe`0io6@FABeB?W1>o;=8{2 z+y3E$@4f1(D}M9We|3F*{XhK3zx|Up{bg3?t4@oM5ehq`+o1+BB&uQX;NdV~8vY^XZg$*Z;sT{yzTBpMT*W{M@B4z2mh%{;#im z^~>J$FaN{i4?fl#4zB;o=ShkX&5;9`tDDGB<_wj2jVdN;0x2N4)gw3VIdlUO13*1q zfBK>KgDCnuS!9@N?DT?x87_+90sqlnhj77#VMIWR3XVzKsmDIX&M&TAUi20ZTyaNt zY0v4$KUp{9jLRm4wf)O`S67}sGhXRKOwJYE9Jq&*B`-5GCGp6o*3Y3UuYTTjZ~VqD zfBiRn6#%TCKJ)uO`Cs4vmbaCVb-atS0EIKoP2ud5>usYz$O1-a1e8<}_P+c%^<$@I z=QogeUg{UCdoFp^tGLstBxJ}bi4l22K^c*{yz9ymQUWk)Y=!cO;yWnOH0d$do@q9B z?!EI`vxVnAXa8$oNoAf>-j2uRD_%@l24;^v{@6~0oOqDgP8<84)S!cKS4gL0b)AW! z<;As&U3YM`FA{yB_urMSyCoYA=*2Ik@vN!04};n2fpqJO{>R?;{sbA7nJ;8<`EV+3 z8Wn$;2W!@tme~#1FPd{m^YDj7;`L}P2r~Lah}wFr8(|C1J8#59!DQK7m+4F+OBvts#?AhQ_Yw z0U;5pP1PL6)+~{Ele9hFdgblU8Fsp}rX~q1H{akbo&CGN*Sa!vmZ&@QLcEY>ZuVfk za7|fsaxr93HuPo@RSg}Okf>zd1AsA@f+hn1Q%jOE=MW)9)u<^VTxg90B0R%em>Z5! zHF@88(e+o~|HP9Dxe;>DF|iq<>F%eYnNdpSSo?$W6Zby;`+xSXYG<}uW`Upy$vJOk zs_MukKqOKyXu`hLwWrRU`OzQ#(I5CXKXh>4zSY&$fBi#0aMevWz4IOKdiv2vt7=wh z(x$oc@LJjBrmY1GAnlIGfenBuTUfhtuy#qezn0|%0KDz*-}ZAq_cMcT&-pBhcp{+Ji6V4GHEW3OH*G|7BNO>SQ%N&2qW_pf+-QWfQkq>7&dMF zuAlhz*OcYa=iPMVj+?&XH-6&8y^mIt^{n4%XO)@mmYhO{&lV`E0kQX4R%XNf{pCZ0 zrGrSEV$)VTXP>?YK*@VECBWUVmibP3f#Zl5WP1Nt6N$L{Ruut_=ZGNh^YbU}2jc#4 zuk&Sp;ozYw^6m8}tI=uInB}x*&)~uPo+z7~HETT1@;)}QKB;!BZFsgg7~K54TVDBP zU+|n)zGTn-J^!O}eDV)|{Fm>1&xgGuiuI_q(o?IHzj)@4zyhad~#v$q!|~ASqJK%7NbX8D7lTcG19!CB8Ty;74zMmgHB+y zs&L#&Oc96)&1d8dXS4JWVn>=wR?7Zu{cV9-YA#!xPTLTpwE)0O#9kxRbu`4X%p{qZ z;KJ(uWMWN?gk}toz-3@9`Lej>H73$N@)-+b>Mkpyb2*v-nFi2Qu?o0cwI(HTy{oR= z_wT;tb8oux(0E!C6FQ=-m_GI7=KDW1d-@b=OC}ati-NJ*TzpAdd;ap`z;(TnkN~vC z0zjYwmQXDsAQA(Rrd^*%nETjR!C1{MeQ9ss)p=?F8FsIC)-6 zB65ht-eqNvUFoxKXSnKI4gfKEEi=DIRgMw+1@<4b>jX| zZ;K(b-|aAf0n!DO&qdGeZhG3qCc;IB#xCUy6oKaYgIxiFL_l;|v9RZ|mAzNHtN?(R zLaL{xv%5d|vA6v0KTN0FnOKY-&=&<(%jIy-+Lf1Ha_jSNz41BEng1L0Wb%$b`TIZq zh2J=H{7i3Y0I^LnG1qSa*b<^HcREYUD_`?{uit;w{-$aiJ2TsL+j;hR_QWsz;hlf= z&ae3Uzddrt^HUos&%ns$=vPApLj#Abpi#vko_*wjKmVGqg6;8`5D>b^Em_bsaCw_z zw?Fv&-~WTvYi?jfN8}Y-O9(jI-ul2>{`$9n;pcH<6;s0{_BFfIU@Ff)t> ziuQrJI$x(-2iXDgqA(*dPX?MHbt7dwKQY0wa3YHU0Tm1e>N@Porm?}|=-`pR%H{EM z59FvA05U0j(J8Twz@6OHF);*7Mp=d~#LAaWTdAXji7BE446&v>@0CRx8lUajF7^%m zLZ4EmF_{880$}zYi8#g-qB0?Xf)aqajB&WKDryjeA(*y=ITk&uM%gvbk$jJAo}uwm zTrNmZC^|@N?MQ)90wP*e^tt2MPGtGu;a~aDFS_Euis5`F2BfA2ROV);^(>7h(lk1q zNY%vAs5*7J(n09nz&OCXL@;TI1rPvK88E3PQTD{dJeSykncB3j&riln`|@jEwA5K< z$7~mf$N(4qyZ|{@Gc+iR>^pw>H{W^p16eqAFRPVhzEf2nyZezl z?|=HEAAisRib$Ihq{vAln*)R(2}DCOgOEa&=ge%TjvW)%F|@Ji_Xl^}{`}jX|GZbc z_{EoAep#>Eh5x-ZIeqrbr|-c%)Lo@JKk}J>{=Gk442KahXJ(?@gPL`VEH>>^CnmSu zcGZ9Qci*zMFgW+n*>g{hHqK6Cs>1#2cD7o`a%2b+`)x7CW|PN_q@5JKV+6=!!kATR zx#6iZrxl;*{@~xg{fUQ8oP6Zr(+{4>2VGw%t4v!-ZH=C{LX|^nri;ZwKOcV4w|~xaUwcD6 zZIKWzgh+#!G3G_*!Tt^F7ROe1lGk0TN-j&*$ zxD=V;lFP4q{#RUn=`FZ<3`X1a@Sq&Lv~%*w&APIzaHB0)T;zo%fA;j|wKw9%S=(Gc zvVZNhJ3E_ukN(-#tehPm%>L`A?mrz{PS5wlu6JXMs%W5nbLi+n9cN4ikhe$(jmJh4 zd3I`L1WzuSdcqIY^~qXp8~T0Z0?(-@N(dRagjymJfEX-^?;*FK7yBFg64b+`Kgwt4 zx}>Q^L}{>XQdV{xWwGAqbXW`DajfRbK2=pxh-~0Xnoc7DIlx+VR2wQ6UKFsYWI%`= z9!;$twX1zNbRY*s?vP~j=sGh+Kt&G>l%gaBpF!rGGa{Jxa}5lg5hIB8R;{zFCmw}r zOXorZ4hV&5d&6Rh5Hl$}dy7kH{=G85ds)G0Y^!nCs|M`19{fWmNq4yly z7TV|$qClKOl@uf<00l;Ag3r7wvIOiscNZ4AWmi-~Y{cxNci;Kmk9_1efBiQOA3Tr` z`d3|k#n*iK=N&z;SCohubq->0+u8K|BgY>4qc@+v^PzM$3~$``lArqen{K`4l`ntc z`#$<9-|dJf0R%-fj4J5xl7oZ0?|$g(zWFDA=EuM7=9{iqxN7A}f-KLD-+TH4|M3Gl z5%8Xb$EmUu2;I!VPCYZB3ACkRm(3ZTmm06x^Rn-JX+1kR-U>*>Wxp(jS=lG@Syr&i z=iVML6-hDFv7I*4oe&z3Ad-lgIS0JR=Dskt1u_)_1e3Y-c2^5Yj06N^2BL~UglaHn zRKrDv|E}Pa08JGTnVglWt+(n&cGe$vSxGJ{I!k4LaN^NXRkvlC6-Ccf+sW+2sZ&p$ zJ$3H%sVDBb=j>xo%|>y7ODJ$*X)jr-t5~2piwy=!&?RZbRUmzYwwgV8|M8n&el2^F zq=dTzYa%qYww~SaJzw1`%8&n-Kbm#@LZ|Qw41f$|)}ahk&v2Zew2f?~c;#StG#irQ zGL;ErmO)P`BaCLlWSz3pwhbOXADx?OdZL+aN?ea&s@5rbEOPgBw$$g#dxKcDqq;tf zJY4M0RL=#wKh~f5)MxfyzW$ug+o$UL`|;$Naj}WgVmm{h%i=O0I!fh0+pxH_chXyE zeJ&fPwnIowyISz%ly-_ES$L|wanL_lWD`rQP)kCv+JPxL;((SV^vp_zpnda=X^zw( ztigmZHx5aOH8%u=V45p9MJ0%!$e;io$N-c~NoQt#p#sT5a#vmPH%sn;ryf|VNRwhG z!vZ{`V`UBv_-V4D;>Lr|bvy=S@easy9j)8zh=_~H1p0fjP|3J4Ps|-RZ7Zy;%(?C2 zHb8SuAVL8=0>I%nbp-boTIW?6eX|AlXc@QW8qq(e? zAAZpqW!zo#4%xso`Pf*hq!~v|S@q!T)MGmxYWqc5mfqzE1jA0IRF8IMz3vb}6@XyA z5D<2+sApYJsf+A>4Xj&nzca$ zds)en5TF6z4qbie-t=Vkm%sj&n}79ttxi!?Fdn^b?^9oR)qDQa+X?;Z!fFqM5rm=9 zn4?qYmNBzQ(`o5ZId^wD{a^px4?pjvx12k_<+2WfX*-)#Q`JN`KNbTLqRkJ%^Hdc= zG-gzC-j(wyHz+HJnvto|ynm+*RCMy9lg}n?5_O(BWw|pOKXl(?N_Ocbht`%Bws)qE z7*P}mf#&=byAbViA;=}E0wFkZ>N2pTvDVm(MmtT-fA`ye|F%DV59e;F><%@XG_zA- z^axB5Y)`SYQ2KnSx3B0_DYDg=awh?ORCGCz2ih?s!X8*F#@L=de!iZ?{z56Ky}0v& z0uq|0+1B*vm%MWH$g$7d{m?m+%Lhdi&KUqBa1Id{L8jcChxR(Za9hz2P1-h5gc0cs z6_v-N(a-}-txn{+h3`06!eUR>MZ`O``iIHZshB!!jC^-98Fo5{fyJy=r+(8Q+5T&B~Qu#;u3 zo5#9dUx%VVSD2c027a*vgPhxOI~8?>&%r?*Iok5Thg#2!Vz(eNvyeK@uy8sKTFFQ{ x&Hb~>4*K5yZe@^yX-av&$UI|0B&^2q{{!*KErye2UUdKf002ovPDHLkV1lIi .md-scroll-mask-bar { + display: block; + position: absolute; + background-color: #fafafa; + right: 0; + top: 0; + bottom: 0; + z-index: 65; + box-shadow: inset 0px 0px 1px rgba(0, 0, 0, 0.3); } + +.md-no-momentum { + -webkit-overflow-scrolling: auto; } + +.md-no-flicker { + -webkit-filter: blur(0px); } + +@media (min-width: 960px) { + .md-padding { + padding: 16px; } } + +html[dir=rtl], html[dir=ltr], body[dir=rtl], body[dir=ltr] { + unicode-bidi: embed; } + +bdo[dir=rtl] { + direction: rtl; + unicode-bidi: bidi-override; } + +bdo[dir=ltr] { + direction: ltr; + unicode-bidi: bidi-override; } + +html, body { + -webkit-tap-highlight-color: transparent; + -webkit-touch-callout: none; + min-height: 100%; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + +/************ + * Headings + ************/ +.md-display-4 { + font-size: 112px; + font-weight: 300; + letter-spacing: -0.010em; + line-height: 112px; } + +.md-display-3 { + font-size: 56px; + font-weight: 400; + letter-spacing: -0.005em; + line-height: 56px; } + +.md-display-2 { + font-size: 45px; + font-weight: 400; + line-height: 64px; } + +.md-display-1 { + font-size: 34px; + font-weight: 400; + line-height: 40px; } + +.md-headline { + font-size: 24px; + font-weight: 400; + line-height: 32px; } + +.md-title { + font-size: 20px; + font-weight: 500; + letter-spacing: 0.005em; } + +.md-subhead { + font-size: 16px; + font-weight: 400; + letter-spacing: 0.010em; + line-height: 24px; } + +/************ + * Body Copy + ************/ +.md-body-1 { + font-size: 14px; + font-weight: 400; + letter-spacing: 0.010em; + line-height: 20px; } + +.md-body-2 { + font-size: 14px; + font-weight: 500; + letter-spacing: 0.010em; + line-height: 24px; } + +.md-caption { + font-size: 12px; + letter-spacing: 0.020em; } + +.md-button { + letter-spacing: 0.010em; } + +/************ + * Defaults + ************/ +button, +select, +html, +textarea, +input { + font-family: Roboto, "Helvetica Neue", sans-serif; } + +select, +button, +textarea, +input { + font-size: 100%; } + +/* +* +* Responsive attributes +* +* References: +* 1) https://scotch.io/tutorials/a-visual-guide-to-css3-flexbox-properties#flex +* 2) https://css-tricks.com/almanac/properties/f/flex/ +* 3) https://css-tricks.com/snippets/css/a-guide-to-flexbox/ +* 4) https://github.com/philipwalton/flexbugs#3-min-height-on-a-flex-container-wont-apply-to-its-flex-items +* 5) http://godban.com.ua/projects/flexgrid +* +* +*/ +@keyframes md-autocomplete-list-out { + 0% { + animation-timing-function: linear; } + 50% { + opacity: 0; + height: 40px; + animation-timing-function: ease-in; } + 100% { + height: 0; + opacity: 0; } } + +@keyframes md-autocomplete-list-in { + 0% { + opacity: 0; + height: 0; + animation-timing-function: ease-out; } + 50% { + opacity: 0; + height: 40px; } + 100% { + opacity: 1; + height: 40px; } } + +md-autocomplete { + border-radius: 2px; + display: block; + height: 40px; + position: relative; + overflow: visible; + min-width: 190px; } + md-autocomplete[disabled] input { + cursor: default; } + md-autocomplete[md-floating-label] { + border-radius: 0; + background: transparent; + height: auto; } + md-autocomplete[md-floating-label] md-input-container { + padding-bottom: 0px; } + md-autocomplete[md-floating-label] md-autocomplete-wrap { + height: auto; } + md-autocomplete[md-floating-label] button { + position: absolute; + top: auto; + bottom: 0; + right: 0; + width: 30px; + height: 30px; } + md-autocomplete md-autocomplete-wrap { + display: flex; + flex-direction: row; + box-sizing: border-box; + position: relative; + overflow: visible; + height: 40px; } + md-autocomplete md-autocomplete-wrap.md-menu-showing { + z-index: 51; } + md-autocomplete md-autocomplete-wrap md-input-container, md-autocomplete md-autocomplete-wrap input { + flex: 1 1 0%; + box-sizing: border-box; + min-width: 0; } + md-autocomplete md-autocomplete-wrap md-progress-linear { + position: absolute; + bottom: -2px; + left: 0; } + md-autocomplete md-autocomplete-wrap md-progress-linear.md-inline { + bottom: 40px; + right: 2px; + left: 2px; + width: auto; } + md-autocomplete md-autocomplete-wrap md-progress-linear .md-mode-indeterminate { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 3px; + transition: none; } + md-autocomplete md-autocomplete-wrap md-progress-linear .md-mode-indeterminate .md-container { + transition: none; + height: 3px; } + md-autocomplete md-autocomplete-wrap md-progress-linear .md-mode-indeterminate.ng-enter { + transition: opacity 0.15s linear; } + md-autocomplete md-autocomplete-wrap md-progress-linear .md-mode-indeterminate.ng-enter.ng-enter-active { + opacity: 1; } + md-autocomplete md-autocomplete-wrap md-progress-linear .md-mode-indeterminate.ng-leave { + transition: opacity 0.15s linear; } + md-autocomplete md-autocomplete-wrap md-progress-linear .md-mode-indeterminate.ng-leave.ng-leave-active { + opacity: 0; } + md-autocomplete input:not(.md-input) { + font-size: 14px; + box-sizing: border-box; + border: none; + box-shadow: none; + outline: none; + background: transparent; + width: 100%; + padding: 0 15px; + line-height: 40px; + height: 40px; } + md-autocomplete input:not(.md-input)::-ms-clear { + display: none; } + md-autocomplete button { + position: relative; + line-height: 20px; + text-align: center; + width: 30px; + height: 30px; + cursor: pointer; + border: none; + border-radius: 50%; + padding: 0; + font-size: 12px; + background: transparent; + margin: auto 5px; } + md-autocomplete button:after { + content: ''; + position: absolute; + top: -6px; + right: -6px; + bottom: -6px; + left: -6px; + border-radius: 50%; + transform: scale(0); + opacity: 0; + transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); } + md-autocomplete button:focus { + outline: none; } + md-autocomplete button:focus:after { + transform: scale(1); + opacity: 1; } + md-autocomplete button md-icon { + position: absolute; + top: 50%; + left: 50%; + transform: translate3d(-50%, -50%, 0) scale(0.9); } + md-autocomplete button md-icon path { + stroke-width: 0; } + md-autocomplete button.ng-enter { + transform: scale(0); + transition: transform 0.15s ease-out; } + md-autocomplete button.ng-enter.ng-enter-active { + transform: scale(1); } + md-autocomplete button.ng-leave { + transition: transform 0.15s ease-out; } + md-autocomplete button.ng-leave.ng-leave-active { + transform: scale(0); } + @media screen and (-ms-high-contrast: active) { + md-autocomplete input { + border: 1px solid #fff; } + md-autocomplete li:focus { + color: #fff; } } + +.md-virtual-repeat-container.md-autocomplete-suggestions-container { + position: absolute; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.25); + height: 225.5px; + max-height: 225.5px; + z-index: 100; } + +.md-virtual-repeat-container.md-not-found { + height: 48px; } + +.md-autocomplete-suggestions { + margin: 0; + list-style: none; + padding: 0; } + .md-autocomplete-suggestions li { + font-size: 14px; + overflow: hidden; + padding: 0 15px; + line-height: 48px; + height: 48px; + transition: background 0.15s linear; + margin: 0; + white-space: nowrap; + text-overflow: ellipsis; } + .md-autocomplete-suggestions li:focus { + outline: none; } + .md-autocomplete-suggestions li:not(.md-not-found-wrapper) { + cursor: pointer; } + +@media screen and (-ms-high-contrast: active) { + md-autocomplete, + .md-autocomplete-suggestions { + border: 1px solid #fff; } } + +md-backdrop { + transition: opacity 450ms; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + z-index: 50; } + md-backdrop.md-menu-backdrop { + position: fixed !important; + z-index: 99; } + md-backdrop.md-select-backdrop { + z-index: 81; + transition-duration: 0; } + md-backdrop.md-dialog-backdrop { + z-index: 79; } + md-backdrop.md-bottom-sheet-backdrop { + z-index: 69; } + md-backdrop.md-sidenav-backdrop { + z-index: 59; } + md-backdrop.md-click-catcher { + position: absolute; } + md-backdrop.md-opaque { + opacity: .48; } + md-backdrop.md-opaque.ng-enter { + opacity: 0; } + md-backdrop.md-opaque.ng-enter.md-opaque.ng-enter-active { + opacity: .48; } + md-backdrop.md-opaque.ng-leave { + opacity: .48; + transition: opacity 400ms; } + md-backdrop.md-opaque.ng-leave.md-opaque.ng-leave-active { + opacity: 0; } + +md-bottom-sheet { + position: absolute; + left: 0; + right: 0; + bottom: 0; + padding: 8px 16px 88px 16px; + z-index: 70; + border-top-width: 1px; + border-top-style: solid; + transform: translate3d(0, 80px, 0); + transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); + transition-property: transform; } + md-bottom-sheet.md-has-header { + padding-top: 0; } + md-bottom-sheet.ng-enter { + opacity: 0; + transform: translate3d(0, 100%, 0); } + md-bottom-sheet.ng-enter-active { + opacity: 1; + display: block; + transform: translate3d(0, 80px, 0) !important; } + md-bottom-sheet.ng-leave-active { + transform: translate3d(0, 100%, 0) !important; + transition: all 0.3s cubic-bezier(0.55, 0, 0.55, 0.2); } + md-bottom-sheet .md-subheader { + background-color: transparent; + font-family: Roboto, "Helvetica Neue", sans-serif; + line-height: 56px; + padding: 0; + white-space: nowrap; } + md-bottom-sheet md-inline-icon { + display: inline-block; + height: 24px; + width: 24px; + fill: #444; } + md-bottom-sheet md-list-item { + display: flex; + outline: none; } + md-bottom-sheet md-list-item:hover { + cursor: pointer; } + md-bottom-sheet.md-list md-list-item { + padding: 0; + align-items: center; + height: 48px; } + md-bottom-sheet.md-grid { + padding-left: 24px; + padding-right: 24px; + padding-top: 0; } + md-bottom-sheet.md-grid md-list { + display: flex; + flex-direction: row; + flex-wrap: wrap; + transition: all 0.5s; + align-items: center; } + md-bottom-sheet.md-grid md-list-item { + flex-direction: column; + align-items: center; + transition: all 0.5s; + height: 96px; + margin-top: 8px; + margin-bottom: 8px; + /* Mixin for how many grid items to show per row */ } + @media (max-width: 960px) { + md-bottom-sheet.md-grid md-list-item { + flex: 1 1 33.3333333333%; + max-width: 33.3333333333%; } + md-bottom-sheet.md-grid md-list-item:nth-of-type(3n + 1) { + align-items: flex-start; } + md-bottom-sheet.md-grid md-list-item:nth-of-type(3n) { + align-items: flex-end; } } + @media (min-width: 960px) and (max-width: 1279px) { + md-bottom-sheet.md-grid md-list-item { + flex: 1 1 25%; + max-width: 25%; } } + @media (min-width: 1280px) and (max-width: 1919px) { + md-bottom-sheet.md-grid md-list-item { + flex: 1 1 16.6666666667%; + max-width: 16.6666666667%; } } + @media (min-width: 1920px) { + md-bottom-sheet.md-grid md-list-item { + flex: 1 1 14.2857142857%; + max-width: 14.2857142857%; } } + md-bottom-sheet.md-grid md-list-item::before { + display: none; } + md-bottom-sheet.md-grid md-list-item .md-list-item-content { + display: flex; + flex-direction: column; + align-items: center; + width: 48px; + padding-bottom: 16px; } + md-bottom-sheet.md-grid md-list-item .md-grid-item-content { + border: 1px solid transparent; + display: flex; + flex-direction: column; + align-items: center; + width: 80px; } + md-bottom-sheet.md-grid md-list-item .md-grid-text { + font-weight: 400; + line-height: 16px; + font-size: 13px; + margin: 0; + white-space: nowrap; + width: 64px; + text-align: center; + text-transform: none; + padding-top: 8px; } + +@media screen and (-ms-high-contrast: active) { + md-bottom-sheet { + border: 1px solid #fff; } } + +button.md-button::-moz-focus-inner { + border: 0; } + +.md-button { + display: inline-block; + position: relative; + cursor: pointer; + /** Alignment adjustments */ + min-height: 36px; + min-width: 88px; + line-height: 36px; + vertical-align: middle; + align-items: center; + text-align: center; + border-radius: 3px; + box-sizing: border-box; + /* Reset default button appearance */ + user-select: none; + outline: none; + border: 0; + /** Custom styling for button */ + padding: 0 6px; + margin: 6px 8px; + background: transparent; + color: currentColor; + white-space: nowrap; + /* Uppercase text content */ + text-transform: uppercase; + font-weight: 500; + font-size: 14px; + font-style: inherit; + font-variant: inherit; + font-family: inherit; + text-decoration: none; + overflow: hidden; + transition: box-shadow 0.4s cubic-bezier(0.25, 0.8, 0.25, 1), background-color 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); } + .md-button:focus { + outline: none; } + .md-button:hover, .md-button:focus { + text-decoration: none; } + .md-button.ng-hide, .md-button.ng-leave { + transition: none; } + .md-button.md-cornered { + border-radius: 0; } + .md-button.md-icon { + padding: 0; + background: none; } + .md-button.md-raised:not([disabled]) { + box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.26); } + .md-button.md-icon-button { + margin: 0 6px; + height: 40px; + min-width: 0; + line-height: 24px; + padding: 8px; + width: 40px; + border-radius: 50%; } + .md-button.md-icon-button .md-ripple-container { + border-radius: 50%; + background-clip: padding-box; + overflow: hidden; + -webkit-mask-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAA5JREFUeNpiYGBgAAgwAAAEAAGbA+oJAAAAAElFTkSuQmCC"); } + .md-button.md-fab { + z-index: 20; + line-height: 56px; + min-width: 0; + width: 56px; + height: 56px; + vertical-align: middle; + box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.26); + border-radius: 50%; + background-clip: padding-box; + overflow: hidden; + transition: all 0.3s cubic-bezier(0.55, 0, 0.55, 0.2); + transition-property: background-color, box-shadow, transform; } + .md-button.md-fab.md-fab-bottom-right { + top: auto; + right: 20px; + bottom: 20px; + left: auto; + position: absolute; } + .md-button.md-fab.md-fab-bottom-left { + top: auto; + right: auto; + bottom: 20px; + left: 20px; + position: absolute; } + .md-button.md-fab.md-fab-top-right { + top: 20px; + right: 20px; + bottom: auto; + left: auto; + position: absolute; } + .md-button.md-fab.md-fab-top-left { + top: 20px; + right: auto; + bottom: auto; + left: 20px; + position: absolute; } + .md-button.md-fab .md-ripple-container { + border-radius: 50%; + background-clip: padding-box; + overflow: hidden; + -webkit-mask-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAA5JREFUeNpiYGBgAAgwAAAEAAGbA+oJAAAAAElFTkSuQmCC"); } + .md-button.md-fab.md-mini { + line-height: 40px; + width: 40px; + height: 40px; } + .md-button.md-fab.ng-hide, .md-button.md-fab.ng-leave { + transition: none; } + .md-button:not([disabled]).md-raised.md-focused, .md-button:not([disabled]).md-fab.md-focused { + box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.26); } + .md-button:not([disabled]).md-raised:active, .md-button:not([disabled]).md-fab:active { + box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.4); } + .md-button .md-ripple-container { + border-radius: 3px; + background-clip: padding-box; + overflow: hidden; + -webkit-mask-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAA5JREFUeNpiYGBgAAgwAAAEAAGbA+oJAAAAAElFTkSuQmCC"); } + +.md-button.md-icon-button md-icon, +button.md-button.md-fab md-icon { + display: block; } + +.md-toast-open-top .md-button.md-fab-top-left, +.md-toast-open-top .md-button.md-fab-top-right { + transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); + transform: translate3d(0, 42px, 0); } + .md-toast-open-top .md-button.md-fab-top-left:not([disabled]).md-focused, .md-toast-open-top .md-button.md-fab-top-left:not([disabled]):hover, + .md-toast-open-top .md-button.md-fab-top-right:not([disabled]).md-focused, + .md-toast-open-top .md-button.md-fab-top-right:not([disabled]):hover { + transform: translate3d(0, 41px, 0); } + +.md-toast-open-bottom .md-button.md-fab-bottom-left, +.md-toast-open-bottom .md-button.md-fab-bottom-right { + transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); + transform: translate3d(0, -42px, 0); } + .md-toast-open-bottom .md-button.md-fab-bottom-left:not([disabled]).md-focused, .md-toast-open-bottom .md-button.md-fab-bottom-left:not([disabled]):hover, + .md-toast-open-bottom .md-button.md-fab-bottom-right:not([disabled]).md-focused, + .md-toast-open-bottom .md-button.md-fab-bottom-right:not([disabled]):hover { + transform: translate3d(0, -43px, 0); } + +.md-button-group { + display: flex; + flex: 1; + width: 100%; } + .md-button-group > .md-button { + flex: 1; + display: block; + overflow: hidden; + width: 0; + border-width: 1px 0px 1px 1px; + border-radius: 0; + text-align: center; + text-overflow: ellipsis; + white-space: nowrap; } + .md-button-group > .md-button:first-child { + border-radius: 2px 0px 0px 2px; } + .md-button-group > .md-button:last-child { + border-right-width: 1px; + border-radius: 0px 2px 2px 0px; } + +@media screen and (-ms-high-contrast: active) { + .md-button.md-raised, + .md-button.md-fab { + border: 1px solid #fff; } } + +md-card { + box-sizing: border-box; + display: flex; + flex-direction: column; + margin: 8px; + box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.2), 0px 1px 1px 0px rgba(0, 0, 0, 0.14), 0px 2px 1px -1px rgba(0, 0, 0, 0.12); } + md-card md-card-header { + padding: 16px; + display: flex; + flex-direction: row; } + md-card md-card-header:first-child md-card-avatar { + margin-right: 12px; } + [dir=rtl] md-card md-card-header:first-child md-card-avatar { + margin-right: auto; + margin-left: 12px; } + md-card md-card-header:last-child md-card-avatar { + margin-left: 12px; } + [dir=rtl] md-card md-card-header:last-child md-card-avatar { + margin-left: auto; + margin-right: 12px; } + md-card md-card-header md-card-avatar { + width: 40px; + height: 40px; } + md-card md-card-header md-card-avatar .md-user-avatar, + md-card md-card-header md-card-avatar md-icon { + border-radius: 50%; } + md-card md-card-header md-card-avatar md-icon { + padding: 8px; } + md-card md-card-header md-card-avatar + md-card-header-text { + max-height: 40px; } + md-card md-card-header md-card-avatar + md-card-header-text .md-title { + font-size: 14px; } + md-card md-card-header md-card-header-text { + display: flex; + flex: 1; + flex-direction: column; } + md-card md-card-header md-card-header-text .md-subhead { + font-size: 14px; } + md-card > img, + md-card > md-card-header img, + md-card md-card-title-media img { + box-sizing: border-box; + display: flex; + flex: 0 0 auto; + width: 100%; + height: auto; } + md-card md-card-title { + padding: 24px 16px 16px; + display: flex; + flex: 1 1 auto; + flex-direction: row; } + md-card md-card-title + md-card-content { + padding-top: 0; } + md-card md-card-title md-card-title-text { + flex: 1; + flex-direction: column; + display: flex; } + md-card md-card-title md-card-title-text .md-subhead { + padding-top: 0; + font-size: 14px; } + md-card md-card-title md-card-title-text:only-child .md-subhead { + padding-top: 12px; } + md-card md-card-title md-card-title-media { + margin-top: -8px; } + md-card md-card-title md-card-title-media .md-media-sm { + height: 80px; + width: 80px; } + md-card md-card-title md-card-title-media .md-media-md { + height: 112px; + width: 112px; } + md-card md-card-title md-card-title-media .md-media-lg { + height: 152px; + width: 152px; } + md-card md-card-content { + display: block; + padding: 16px; } + md-card md-card-content > p:first-child { + margin-top: 0; } + md-card md-card-content > p:last-child { + margin-bottom: 0; } + md-card md-card-content .md-media-xl { + height: 240px; + width: 240px; } + md-card .md-actions, md-card md-card-actions { + margin: 8px; } + md-card .md-actions.layout-column .md-button:not(.md-icon-button), md-card md-card-actions.layout-column .md-button:not(.md-icon-button) { + margin: 2px 0; } + md-card .md-actions.layout-column .md-button:not(.md-icon-button):first-of-type, md-card md-card-actions.layout-column .md-button:not(.md-icon-button):first-of-type { + margin-top: 0; } + md-card .md-actions.layout-column .md-button:not(.md-icon-button):last-of-type, md-card md-card-actions.layout-column .md-button:not(.md-icon-button):last-of-type { + margin-bottom: 0; } + md-card .md-actions.layout-column .md-button.md-icon-button, md-card md-card-actions.layout-column .md-button.md-icon-button { + margin-top: 6px; + margin-bottom: 6px; } + md-card .md-actions md-card-icon-actions, md-card md-card-actions md-card-icon-actions { + flex: 1; + justify-content: flex-start; + display: flex; + flex-direction: row; } + md-card .md-actions:not(.layout-column) .md-button:not(.md-icon-button), md-card md-card-actions:not(.layout-column) .md-button:not(.md-icon-button) { + margin: 0 4px; } + md-card .md-actions:not(.layout-column) .md-button:not(.md-icon-button):first-of-type, md-card md-card-actions:not(.layout-column) .md-button:not(.md-icon-button):first-of-type { + margin-left: 0; } + [dir=rtl] md-card .md-actions:not(.layout-column) .md-button:not(.md-icon-button):first-of-type, [dir=rtl] md-card md-card-actions:not(.layout-column) .md-button:not(.md-icon-button):first-of-type { + margin-left: auto; + margin-right: 0; } + md-card .md-actions:not(.layout-column) .md-button:not(.md-icon-button):last-of-type, md-card md-card-actions:not(.layout-column) .md-button:not(.md-icon-button):last-of-type { + margin-right: 0; } + [dir=rtl] md-card .md-actions:not(.layout-column) .md-button:not(.md-icon-button):last-of-type, [dir=rtl] md-card md-card-actions:not(.layout-column) .md-button:not(.md-icon-button):last-of-type { + margin-right: auto; + margin-left: 0; } + md-card .md-actions:not(.layout-column) .md-button.md-icon-button, md-card md-card-actions:not(.layout-column) .md-button.md-icon-button { + margin-left: 6px; + margin-right: 6px; } + md-card .md-actions:not(.layout-column) .md-button.md-icon-button:first-of-type, md-card md-card-actions:not(.layout-column) .md-button.md-icon-button:first-of-type { + margin-left: 12px; } + [dir=rtl] md-card .md-actions:not(.layout-column) .md-button.md-icon-button:first-of-type, [dir=rtl] md-card md-card-actions:not(.layout-column) .md-button.md-icon-button:first-of-type { + margin-left: auto; + margin-right: 12px; } + md-card .md-actions:not(.layout-column) .md-button.md-icon-button:last-of-type, md-card md-card-actions:not(.layout-column) .md-button.md-icon-button:last-of-type { + margin-right: 12px; } + [dir=rtl] md-card .md-actions:not(.layout-column) .md-button.md-icon-button:last-of-type, [dir=rtl] md-card md-card-actions:not(.layout-column) .md-button.md-icon-button:last-of-type { + margin-right: auto; + margin-left: 12px; } + md-card .md-actions:not(.layout-column) .md-button + md-card-icon-actions, md-card md-card-actions:not(.layout-column) .md-button + md-card-icon-actions { + flex: 1; + justify-content: flex-end; + display: flex; + flex-direction: row; } + md-card md-card-footer { + margin-top: auto; + padding: 16px; } + +@media screen and (-ms-high-contrast: active) { + md-card { + border: 1px solid #fff; } } + +.md-image-no-fill > img { + width: auto; + height: auto; } + +.md-contact-chips .md-chips md-chip { + padding: 0 25px 0 0; } + [dir=rtl] .md-contact-chips .md-chips md-chip { + padding: 0 0 0 25px; } + .md-contact-chips .md-chips md-chip .md-contact-avatar { + float: left; } + [dir=rtl] .md-contact-chips .md-chips md-chip .md-contact-avatar { + float: right; } + .md-contact-chips .md-chips md-chip .md-contact-avatar img { + height: 32px; + border-radius: 16px; } + .md-contact-chips .md-chips md-chip .md-contact-name { + display: inline-block; + height: 32px; + margin-left: 8px; } + [dir=rtl] .md-contact-chips .md-chips md-chip .md-contact-name { + margin-left: auto; + margin-right: 8px; } + +.md-contact-suggestion { + height: 56px; } + .md-contact-suggestion img { + height: 40px; + border-radius: 20px; + margin-top: 8px; } + .md-contact-suggestion .md-contact-name { + margin-left: 8px; + width: 120px; } + [dir=rtl] .md-contact-suggestion .md-contact-name { + margin-left: auto; + margin-right: 8px; } + .md-contact-suggestion .md-contact-name, .md-contact-suggestion .md-contact-email { + display: inline-block; + overflow: hidden; + text-overflow: ellipsis; } + +.md-contact-chips-suggestions li { + height: 100%; } + +.md-chips { + display: block; + font-family: Roboto, "Helvetica Neue", sans-serif; + font-size: 16px; + padding: 0 0 8px 3px; + vertical-align: middle; } + .md-chips:after { + content: ''; + display: table; + clear: both; } + [dir=rtl] .md-chips { + padding: 0 3px 8px 0; } + .md-chips.md-readonly .md-chip-input-container { + min-height: 32px; } + .md-chips:not(.md-readonly) { + cursor: text; } + .md-chips.md-removable md-chip { + padding-right: 22px; } + [dir=rtl] .md-chips.md-removable md-chip { + padding-right: 0; + padding-left: 22px; } + .md-chips.md-removable md-chip .md-chip-content { + padding-right: 4px; } + [dir=rtl] .md-chips.md-removable md-chip .md-chip-content { + padding-right: 0; + padding-left: 4px; } + .md-chips md-chip { + cursor: default; + border-radius: 16px; + display: block; + height: 32px; + line-height: 32px; + margin: 8px 8px 0 0; + padding: 0 12px 0 12px; + float: left; + box-sizing: border-box; + max-width: 100%; + position: relative; } + [dir=rtl] .md-chips md-chip { + margin: 8px 0 0 8px; } + [dir=rtl] .md-chips md-chip { + float: right; } + .md-chips md-chip .md-chip-content { + display: block; + float: left; + white-space: nowrap; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; } + [dir=rtl] .md-chips md-chip .md-chip-content { + float: right; } + .md-chips md-chip .md-chip-content:focus { + outline: none; } + .md-chips md-chip._md-chip-content-edit-is-enabled { + -webkit-user-select: none; + /* webkit (safari, chrome) browsers */ + -moz-user-select: none; + /* mozilla browsers */ + -khtml-user-select: none; + /* webkit (konqueror) browsers */ + -ms-user-select: none; + /* IE10+ */ } + .md-chips md-chip .md-chip-remove-container { + position: absolute; + right: 0; + line-height: 22px; } + [dir=rtl] .md-chips md-chip .md-chip-remove-container { + right: auto; + left: 0; } + .md-chips md-chip .md-chip-remove { + text-align: center; + width: 32px; + height: 32px; + min-width: 0; + padding: 0; + background: transparent; + border: none; + box-shadow: none; + margin: 0; + position: relative; } + .md-chips md-chip .md-chip-remove md-icon { + height: 18px; + width: 18px; + position: absolute; + top: 50%; + left: 50%; + transform: translate3d(-50%, -50%, 0); } + .md-chips .md-chip-input-container { + display: block; + line-height: 32px; + margin: 8px 8px 0 0; + padding: 0; + float: left; } + [dir=rtl] .md-chips .md-chip-input-container { + margin: 8px 0 0 8px; } + [dir=rtl] .md-chips .md-chip-input-container { + float: right; } + .md-chips .md-chip-input-container input:not([type]), .md-chips .md-chip-input-container input[type="email"], .md-chips .md-chip-input-container input[type="number"], .md-chips .md-chip-input-container input[type="tel"], .md-chips .md-chip-input-container input[type="url"], .md-chips .md-chip-input-container input[type="text"] { + border: 0; + height: 32px; + line-height: 32px; + padding: 0; } + .md-chips .md-chip-input-container input:not([type]):focus, .md-chips .md-chip-input-container input[type="email"]:focus, .md-chips .md-chip-input-container input[type="number"]:focus, .md-chips .md-chip-input-container input[type="tel"]:focus, .md-chips .md-chip-input-container input[type="url"]:focus, .md-chips .md-chip-input-container input[type="text"]:focus { + outline: none; } + .md-chips .md-chip-input-container md-autocomplete, .md-chips .md-chip-input-container md-autocomplete-wrap { + background: transparent; + height: 32px; } + .md-chips .md-chip-input-container md-autocomplete md-autocomplete-wrap { + box-shadow: none; } + .md-chips .md-chip-input-container md-autocomplete input { + position: relative; } + .md-chips .md-chip-input-container input { + border: 0; + height: 32px; + line-height: 32px; + padding: 0; } + .md-chips .md-chip-input-container input:focus { + outline: none; } + .md-chips .md-chip-input-container md-autocomplete, .md-chips .md-chip-input-container md-autocomplete-wrap { + height: 32px; } + .md-chips .md-chip-input-container md-autocomplete { + box-shadow: none; } + .md-chips .md-chip-input-container md-autocomplete input { + position: relative; } + .md-chips .md-chip-input-container:not(:first-child) { + margin: 8px 8px 0 0; } + [dir=rtl] .md-chips .md-chip-input-container:not(:first-child) { + margin: 8px 0 0 8px; } + .md-chips .md-chip-input-container input { + background: transparent; + border-width: 0; } + .md-chips md-autocomplete button { + display: none; } + +@media screen and (-ms-high-contrast: active) { + .md-chip-input-container, + md-chip { + border: 1px solid #fff; } + .md-chip-input-container md-autocomplete { + border: none; } } + +.md-inline-form md-checkbox { + margin: 19px 0 18px; } + +md-checkbox { + box-sizing: border-box; + display: inline-block; + margin-bottom: 16px; + white-space: nowrap; + cursor: pointer; + outline: none; + user-select: none; + position: relative; + min-width: 20px; + min-height: 20px; + margin-left: 0; + margin-right: 16px; } + [dir=rtl] md-checkbox { + margin-left: 16px; } + [dir=rtl] md-checkbox { + margin-right: 0; } + md-checkbox:last-of-type { + margin-left: 0; + margin-right: 0; } + md-checkbox.md-focused:not([disabled]) .md-container:before { + left: -8px; + top: -8px; + right: -8px; + bottom: -8px; } + md-checkbox.md-focused:not([disabled]):not(.md-checked) .md-container:before { + background-color: rgba(0, 0, 0, 0.12); } + md-checkbox.md-align-top-left > div.md-container { + top: 12px; } + md-checkbox .md-container { + position: absolute; + top: 50%; + transform: translateY(-50%); + box-sizing: border-box; + display: inline-block; + width: 20px; + height: 20px; + left: 0; + right: auto; } + [dir=rtl] md-checkbox .md-container { + left: auto; } + [dir=rtl] md-checkbox .md-container { + right: 0; } + md-checkbox .md-container:before { + box-sizing: border-box; + background-color: transparent; + border-radius: 50%; + content: ''; + position: absolute; + display: block; + height: auto; + left: 0; + top: 0; + right: 0; + bottom: 0; + transition: all 0.5s; + width: auto; } + md-checkbox .md-container:after { + box-sizing: border-box; + content: ''; + position: absolute; + top: -10px; + right: -10px; + bottom: -10px; + left: -10px; } + md-checkbox .md-container .md-ripple-container { + position: absolute; + display: block; + width: auto; + height: auto; + left: -15px; + top: -15px; + right: -15px; + bottom: -15px; } + md-checkbox .md-icon { + box-sizing: border-box; + transition: 240ms; + position: absolute; + top: 0; + left: 0; + width: 20px; + height: 20px; + border-width: 2px; + border-style: solid; + border-radius: 2px; } + md-checkbox.md-checked .md-icon { + border-color: transparent; } + md-checkbox.md-checked .md-icon:after { + box-sizing: border-box; + transform: rotate(45deg); + position: absolute; + left: 4.6666666667px; + top: 0.2222222222px; + display: table; + width: 6.6666666667px; + height: 13.3333333333px; + border-width: 2px; + border-style: solid; + border-top: 0; + border-left: 0; + content: ''; } + md-checkbox[disabled] { + cursor: default; } + md-checkbox.md-indeterminate .md-icon:after { + box-sizing: border-box; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + display: table; + width: 12px; + height: 2px; + border-width: 2px; + border-style: solid; + border-top: 0; + border-left: 0; + content: ''; } + md-checkbox .md-label { + box-sizing: border-box; + position: relative; + display: inline-block; + vertical-align: middle; + white-space: normal; + user-select: text; + margin-left: 30px; + margin-right: 0; } + [dir=rtl] md-checkbox .md-label { + margin-left: 0; } + [dir=rtl] md-checkbox .md-label { + margin-right: 30px; } + +md-content { + display: block; + position: relative; + overflow: auto; + -webkit-overflow-scrolling: touch; } + md-content[md-scroll-y] { + overflow-y: auto; + overflow-x: hidden; } + md-content[md-scroll-x] { + overflow-x: auto; + overflow-y: hidden; } + @media print { + md-content { + overflow: visible !important; } } + +/** Styles for mdCalendar. */ +md-calendar { + font-size: 13px; + user-select: none; } + +.md-calendar-scroll-mask { + display: inline-block; + overflow: hidden; + height: 308px; } + .md-calendar-scroll-mask .md-virtual-repeat-scroller { + overflow-y: scroll; + -webkit-overflow-scrolling: touch; } + .md-calendar-scroll-mask .md-virtual-repeat-scroller::-webkit-scrollbar { + display: none; } + .md-calendar-scroll-mask .md-virtual-repeat-offsetter { + width: 100%; } + +.md-calendar-scroll-container { + box-shadow: inset -3px 3px 6px rgba(0, 0, 0, 0.2); + display: inline-block; + height: 308px; + width: 346px; } + +.md-calendar-date { + height: 44px; + width: 44px; + text-align: center; + padding: 0; + border: none; + box-sizing: content-box; } + .md-calendar-date:first-child { + padding-left: 16px; } + [dir=rtl] .md-calendar-date:first-child { + padding-left: 0; + padding-right: 16px; } + .md-calendar-date:last-child { + padding-right: 16px; } + [dir=rtl] .md-calendar-date:last-child { + padding-right: 0; + padding-left: 16px; } + .md-calendar-date.md-calendar-date-disabled { + cursor: default; } + +.md-calendar-date-selection-indicator { + transition: background-color, color 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); + border-radius: 50%; + display: inline-block; + width: 40px; + height: 40px; + line-height: 40px; } + .md-calendar-date:not(.md-disabled) .md-calendar-date-selection-indicator { + cursor: pointer; } + +.md-calendar-month-label { + height: 44px; + font-size: 14px; + font-weight: 500; + padding: 0 0 0 24px; } + [dir=rtl] .md-calendar-month-label { + padding: 0 24px 0 0; } + md-calendar-month .md-calendar-month-label:not(.md-calendar-month-label-disabled) { + cursor: pointer; } + .md-calendar-month-label md-icon { + transform: rotate(180deg); } + [dir=rtl] .md-calendar-month-label md-icon { + transform: none; } + .md-calendar-month-label span { + vertical-align: middle; } + +.md-calendar-day-header { + table-layout: fixed; + border-spacing: 0; + border-collapse: collapse; } + .md-calendar-day-header th { + height: 40px; + width: 44px; + text-align: center; + padding: 0; + border: none; + box-sizing: content-box; + font-weight: normal; } + .md-calendar-day-header th:first-child { + padding-left: 16px; } + [dir=rtl] .md-calendar-day-header th:first-child { + padding-left: 0; + padding-right: 16px; } + .md-calendar-day-header th:last-child { + padding-right: 16px; } + [dir=rtl] .md-calendar-day-header th:last-child { + padding-right: 0; + padding-left: 16px; } + +.md-calendar { + table-layout: fixed; + border-spacing: 0; + border-collapse: collapse; } + .md-calendar tr:last-child td { + border-bottom-width: 1px; + border-bottom-style: solid; } + .md-calendar:first-child { + border-top: 1px solid transparent; } + .md-calendar tbody, .md-calendar td, .md-calendar tr { + vertical-align: middle; + box-sizing: content-box; } + +/** Styles for mdDatepicker. */ +md-datepicker { + white-space: nowrap; + overflow: hidden; + padding-right: 18px; + margin-right: -18px; + vertical-align: middle; } + [dir=rtl] md-datepicker { + padding-right: 0; + padding-left: 18px; } + [dir=rtl] md-datepicker { + margin-right: auto; + margin-left: -18px; } + +.md-inline-form md-datepicker { + margin-top: 12px; } + +.md-datepicker-button { + display: inline-block; + box-sizing: border-box; + background: none; + vertical-align: middle; + position: relative; } + .md-datepicker-button:before { + top: 0; + left: 0; + bottom: 0; + right: 0; + position: absolute; + content: ''; + speak: none; } + +.md-datepicker-input { + font-size: 14px; + box-sizing: border-box; + border: none; + box-shadow: none; + outline: none; + background: transparent; + min-width: 120px; + max-width: 328px; + padding: 0 0 5px; } + .md-datepicker-input::-ms-clear { + display: none; } + +._md-datepicker-floating-label > md-datepicker { + overflow: visible; } + ._md-datepicker-floating-label > md-datepicker .md-datepicker-input-container { + border: none; } + ._md-datepicker-floating-label > md-datepicker .md-datepicker-button { + float: left; + margin-top: -2.5px; } + [dir=rtl] ._md-datepicker-floating-label > md-datepicker .md-datepicker-button { + float: right; } + +._md-datepicker-floating-label._md-datepicker-has-calendar-icon > label:not(.md-no-float):not(.md-container-ignore) { + right: 18px; + left: auto; + width: calc(100% - 84px); } + [dir=rtl] ._md-datepicker-floating-label._md-datepicker-has-calendar-icon > label:not(.md-no-float):not(.md-container-ignore) { + right: auto; } + [dir=rtl] ._md-datepicker-floating-label._md-datepicker-has-calendar-icon > label:not(.md-no-float):not(.md-container-ignore) { + left: 18px; } + +._md-datepicker-floating-label._md-datepicker-has-calendar-icon .md-input-message-animation { + margin-left: 64px; } + [dir=rtl] ._md-datepicker-floating-label._md-datepicker-has-calendar-icon .md-input-message-animation { + margin-left: auto; + margin-right: 64px; } + +.md-datepicker-input-container { + position: relative; + border-bottom-width: 1px; + border-bottom-style: solid; + display: inline-block; + width: auto; } + .md-icon-button + .md-datepicker-input-container { + margin-left: 12px; } + [dir=rtl] .md-icon-button + .md-datepicker-input-container { + margin-left: auto; + margin-right: 12px; } + .md-datepicker-input-container.md-datepicker-focused { + border-bottom-width: 2px; } + +.md-datepicker-is-showing .md-scroll-mask { + z-index: 99; } + +.md-datepicker-calendar-pane { + position: absolute; + top: 0; + left: -100%; + z-index: 100; + border-width: 1px; + border-style: solid; + background: transparent; + transform: scale(0); + transform-origin: 0 0; + transition: transform 0.2s cubic-bezier(0.25, 0.8, 0.25, 1); } + .md-datepicker-calendar-pane.md-pane-open { + transform: scale(1); } + +.md-datepicker-input-mask { + height: 40px; + width: 340px; + position: relative; + overflow: hidden; + background: transparent; + pointer-events: none; + cursor: text; } + +.md-datepicker-calendar { + opacity: 0; + transition: opacity 0.2s cubic-bezier(0.5, 0, 0.25, 1); } + .md-pane-open .md-datepicker-calendar { + opacity: 1; } + .md-datepicker-calendar md-calendar:focus { + outline: none; } + +.md-datepicker-expand-triangle { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 0; + height: 0; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 5px solid; } + +.md-datepicker-triangle-button { + position: absolute; + right: 0; + top: 5px; + transform: translateY(-25%) translateX(45%); } + [dir=rtl] .md-datepicker-triangle-button { + right: auto; + left: 0; } + [dir=rtl] .md-datepicker-triangle-button { + transform: translateY(-25%) translateX(-45%); } + +.md-datepicker-triangle-button.md-button.md-icon-button { + height: 36px; + width: 36px; + position: absolute; + padding: 8px; } + +md-datepicker[disabled] .md-datepicker-input-container { + border-bottom-color: transparent; } + +md-datepicker[disabled] .md-datepicker-triangle-button { + display: none; } + +.md-datepicker-open { + overflow: hidden; } + .md-datepicker-open .md-datepicker-input-container, + .md-datepicker-open input.md-input { + border-bottom-color: transparent; } + .md-datepicker-open .md-datepicker-triangle-button, + .md-datepicker-open.md-input-has-value > label, + .md-datepicker-open.md-input-has-placeholder > label { + display: none; } + +.md-datepicker-pos-adjusted .md-datepicker-input-mask { + display: none; } + +.md-datepicker-calendar-pane .md-calendar { + transform: translateY(-85px); + transition: transform 0.65s cubic-bezier(0.25, 0.8, 0.25, 1); + transition-delay: 0.125s; } + +.md-datepicker-calendar-pane.md-pane-open .md-calendar { + transform: translateY(0); } + +.md-dialog-is-showing { + max-height: 100%; } + +.md-dialog-container { + display: flex; + justify-content: center; + align-items: center; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 80; + overflow: hidden; } + +md-dialog { + opacity: 0; + min-width: 240px; + max-width: 80%; + max-height: 80%; + position: relative; + overflow: auto; + box-shadow: 0px 7px 8px -4px rgba(0, 0, 0, 0.2), 0px 13px 19px 2px rgba(0, 0, 0, 0.14), 0px 5px 24px 4px rgba(0, 0, 0, 0.12); + display: flex; + flex-direction: column; } + md-dialog.md-transition-in { + opacity: 1; + transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); + transform: translate(0, 0) scale(1); } + md-dialog.md-transition-out { + opacity: 0; + transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); + transform: translate(0, 100%) scale(0.2); } + md-dialog > form { + display: flex; + flex-direction: column; + overflow: auto; } + md-dialog .md-dialog-content { + padding: 24px; } + md-dialog md-dialog-content { + order: 1; + flex-direction: column; + overflow: auto; + -webkit-overflow-scrolling: touch; } + md-dialog md-dialog-content:not([layout=row]) > *:first-child:not(.md-subheader) { + margin-top: 0; } + md-dialog md-dialog-content:focus { + outline: none; } + md-dialog md-dialog-content .md-subheader { + margin: 0; } + md-dialog md-dialog-content .md-dialog-content-body { + width: 100%; } + md-dialog md-dialog-content .md-prompt-input-container { + width: 100%; + box-sizing: border-box; } + md-dialog .md-actions, md-dialog md-dialog-actions { + display: flex; + order: 2; + box-sizing: border-box; + align-items: center; + justify-content: flex-end; + margin-bottom: 0; + padding-right: 8px; + padding-left: 16px; + min-height: 52px; + overflow: hidden; } + [dir=rtl] md-dialog .md-actions, [dir=rtl] md-dialog md-dialog-actions { + padding-right: 16px; } + [dir=rtl] md-dialog .md-actions, [dir=rtl] md-dialog md-dialog-actions { + padding-left: 8px; } + md-dialog .md-actions .md-button, md-dialog md-dialog-actions .md-button { + margin-bottom: 8px; + margin-left: 8px; + margin-right: 0; + margin-top: 8px; } + [dir=rtl] md-dialog .md-actions .md-button, [dir=rtl] md-dialog md-dialog-actions .md-button { + margin-left: 0; } + [dir=rtl] md-dialog .md-actions .md-button, [dir=rtl] md-dialog md-dialog-actions .md-button { + margin-right: 8px; } + md-dialog.md-content-overflow .md-actions, md-dialog.md-content-overflow md-dialog-actions { + border-top-width: 1px; + border-top-style: solid; } + +@media screen and (-ms-high-contrast: active) { + md-dialog { + border: 1px solid #fff; } } + +@media (max-width: 959px) { + md-dialog.md-dialog-fullscreen { + min-height: 100%; + min-width: 100%; + border-radius: 0; } } + +md-divider { + display: block; + border-top-width: 1px; + border-top-style: solid; + margin: 0; } + md-divider[md-inset] { + margin-left: 80px; } + [dir=rtl] md-divider[md-inset] { + margin-left: auto; + margin-right: 80px; } + +.layout-row > md-divider, +.layout-xs-row > md-divider, .layout-gt-xs-row > md-divider, +.layout-sm-row > md-divider, .layout-gt-sm-row > md-divider, +.layout-md-row > md-divider, .layout-gt-md-row > md-divider, +.layout-lg-row > md-divider, .layout-gt-lg-row > md-divider, +.layout-xl-row > md-divider { + border-top-width: 0; + border-right-width: 1px; + border-right-style: solid; } + +md-fab-speed-dial { + position: relative; + display: flex; + align-items: center; + z-index: 20; + /* + * Hide some graphics glitches if switching animation types + */ + /* + * Handle the animations + */ } + md-fab-speed-dial.md-fab-bottom-right { + top: auto; + right: 20px; + bottom: 20px; + left: auto; + position: absolute; } + md-fab-speed-dial.md-fab-bottom-left { + top: auto; + right: auto; + bottom: 20px; + left: 20px; + position: absolute; } + md-fab-speed-dial.md-fab-top-right { + top: 20px; + right: 20px; + bottom: auto; + left: auto; + position: absolute; } + md-fab-speed-dial.md-fab-top-left { + top: 20px; + right: auto; + bottom: auto; + left: 20px; + position: absolute; } + md-fab-speed-dial:not(.md-hover-full) { + pointer-events: none; } + md-fab-speed-dial:not(.md-hover-full) md-fab-trigger, md-fab-speed-dial:not(.md-hover-full) .md-fab-action-item { + pointer-events: auto; } + md-fab-speed-dial:not(.md-hover-full).md-is-open { + pointer-events: auto; } + md-fab-speed-dial ._md-css-variables { + z-index: 20; } + md-fab-speed-dial.md-is-open .md-fab-action-item { + align-items: center; } + md-fab-speed-dial md-fab-actions { + display: flex; + height: auto; } + md-fab-speed-dial md-fab-actions .md-fab-action-item { + transition: all 0.3s cubic-bezier(0.55, 0, 0.55, 0.2); } + md-fab-speed-dial.md-down { + flex-direction: column; } + md-fab-speed-dial.md-down md-fab-trigger { + order: 1; } + md-fab-speed-dial.md-down md-fab-actions { + flex-direction: column; + order: 2; } + md-fab-speed-dial.md-up { + flex-direction: column; } + md-fab-speed-dial.md-up md-fab-trigger { + order: 2; } + md-fab-speed-dial.md-up md-fab-actions { + flex-direction: column-reverse; + order: 1; } + md-fab-speed-dial.md-left { + flex-direction: row; } + md-fab-speed-dial.md-left md-fab-trigger { + order: 2; } + md-fab-speed-dial.md-left md-fab-actions { + flex-direction: row-reverse; + order: 1; } + md-fab-speed-dial.md-left md-fab-actions .md-fab-action-item { + transition: all 0.3s cubic-bezier(0.55, 0, 0.55, 0.2); } + md-fab-speed-dial.md-right { + flex-direction: row; } + md-fab-speed-dial.md-right md-fab-trigger { + order: 1; } + md-fab-speed-dial.md-right md-fab-actions { + flex-direction: row; + order: 2; } + md-fab-speed-dial.md-right md-fab-actions .md-fab-action-item { + transition: all 0.3s cubic-bezier(0.55, 0, 0.55, 0.2); } + md-fab-speed-dial.md-fling-remove .md-fab-action-item > *, md-fab-speed-dial.md-scale-remove .md-fab-action-item > * { + visibility: hidden; } + md-fab-speed-dial.md-fling .md-fab-action-item { + opacity: 1; } + md-fab-speed-dial.md-fling.md-animations-waiting .md-fab-action-item { + opacity: 0; + transition-duration: 0s; } + md-fab-speed-dial.md-scale .md-fab-action-item { + transform: scale(0); + transition: all 0.3s cubic-bezier(0.55, 0, 0.55, 0.2); + transition-duration: 0.1428571429s; } + +md-fab-toolbar { + display: block; + /* + * Closed styling + */ + /* + * Hover styling + */ } + md-fab-toolbar.md-fab-bottom-right { + top: auto; + right: 20px; + bottom: 20px; + left: auto; + position: absolute; } + md-fab-toolbar.md-fab-bottom-left { + top: auto; + right: auto; + bottom: 20px; + left: 20px; + position: absolute; } + md-fab-toolbar.md-fab-top-right { + top: 20px; + right: 20px; + bottom: auto; + left: auto; + position: absolute; } + md-fab-toolbar.md-fab-top-left { + top: 20px; + right: auto; + bottom: auto; + left: 20px; + position: absolute; } + md-fab-toolbar .md-fab-toolbar-wrapper { + display: block; + position: relative; + overflow: hidden; + height: 68px; } + md-fab-toolbar md-fab-trigger { + position: absolute; + z-index: 20; } + md-fab-toolbar md-fab-trigger button { + overflow: visible !important; } + md-fab-toolbar md-fab-trigger .md-fab-toolbar-background { + display: block; + position: absolute; + z-index: 21; + opacity: 1; + transition: all 0.3s cubic-bezier(0.55, 0, 0.55, 0.2); } + md-fab-toolbar md-fab-trigger md-icon { + position: relative; + z-index: 22; + opacity: 1; + transition: all 200ms ease-in; } + md-fab-toolbar.md-left md-fab-trigger { + right: 0; } + [dir=rtl] md-fab-toolbar.md-left md-fab-trigger { + right: auto; + left: 0; } + md-fab-toolbar.md-left .md-toolbar-tools { + flex-direction: row-reverse; } + md-fab-toolbar.md-left .md-toolbar-tools > .md-button:first-child { + margin-right: 0.6rem; } + [dir=rtl] md-fab-toolbar.md-left .md-toolbar-tools > .md-button:first-child { + margin-right: auto; + margin-left: 0.6rem; } + md-fab-toolbar.md-left .md-toolbar-tools > .md-button:first-child { + margin-left: -0.8rem; } + [dir=rtl] md-fab-toolbar.md-left .md-toolbar-tools > .md-button:first-child { + margin-left: auto; + margin-right: -0.8rem; } + md-fab-toolbar.md-left .md-toolbar-tools > .md-button:last-child { + margin-right: 8px; } + [dir=rtl] md-fab-toolbar.md-left .md-toolbar-tools > .md-button:last-child { + margin-right: auto; + margin-left: 8px; } + md-fab-toolbar.md-right md-fab-trigger { + left: 0; } + [dir=rtl] md-fab-toolbar.md-right md-fab-trigger { + left: auto; + right: 0; } + md-fab-toolbar.md-right .md-toolbar-tools { + flex-direction: row; } + md-fab-toolbar md-toolbar { + background-color: transparent !important; + pointer-events: none; + z-index: 23; } + md-fab-toolbar md-toolbar .md-toolbar-tools { + padding: 0 20px; + margin-top: 3px; } + md-fab-toolbar md-toolbar .md-fab-action-item { + opacity: 0; + transform: scale(0); + transition: all 0.3s cubic-bezier(0.55, 0, 0.55, 0.2); + transition-duration: 0.15s; } + md-fab-toolbar.md-is-open md-fab-trigger > button { + box-shadow: none; } + md-fab-toolbar.md-is-open md-fab-trigger > button md-icon { + opacity: 0; } + md-fab-toolbar.md-is-open .md-fab-action-item { + opacity: 1; + transform: scale(1); } + +md-grid-list { + box-sizing: border-box; + display: block; + position: relative; } + md-grid-list md-grid-tile, + md-grid-list md-grid-tile > figure, + md-grid-list md-grid-tile-header, + md-grid-list md-grid-tile-footer { + box-sizing: border-box; } + md-grid-list md-grid-tile { + display: block; + position: absolute; } + md-grid-list md-grid-tile figure { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + padding: 0; + margin: 0; } + md-grid-list md-grid-tile md-grid-tile-header, + md-grid-list md-grid-tile md-grid-tile-footer { + display: flex; + flex-direction: row; + align-items: center; + height: 48px; + color: #fff; + background: rgba(0, 0, 0, 0.18); + overflow: hidden; + position: absolute; + left: 0; + right: 0; } + md-grid-list md-grid-tile md-grid-tile-header h3, + md-grid-list md-grid-tile md-grid-tile-header h4, + md-grid-list md-grid-tile md-grid-tile-footer h3, + md-grid-list md-grid-tile md-grid-tile-footer h4 { + font-weight: 400; + margin: 0 0 0 16px; } + md-grid-list md-grid-tile md-grid-tile-header h3, + md-grid-list md-grid-tile md-grid-tile-footer h3 { + font-size: 14px; } + md-grid-list md-grid-tile md-grid-tile-header h4, + md-grid-list md-grid-tile md-grid-tile-footer h4 { + font-size: 12px; } + md-grid-list md-grid-tile md-grid-tile-header { + top: 0; } + md-grid-list md-grid-tile md-grid-tile-footer { + bottom: 0; } + +@media screen and (-ms-high-contrast: active) { + md-grid-tile { + border: 1px solid #fff; } + md-grid-tile-footer { + border-top: 1px solid #fff; } } + +md-icon { + margin: auto; + background-repeat: no-repeat no-repeat; + display: inline-block; + vertical-align: middle; + fill: currentColor; + height: 24px; + width: 24px; + min-height: 24px; + min-width: 24px; } + md-icon svg { + pointer-events: none; + display: block; } + md-icon[md-font-icon] { + line-height: 24px; + width: auto; } + +md-input-container { + display: inline-block; + position: relative; + padding: 2px; + margin: 18px 0; + vertical-align: middle; + /* + * The .md-input class is added to the input/textarea + */ } + md-input-container:after { + content: ''; + display: table; + clear: both; } + md-input-container.md-block { + display: block; } + md-input-container .md-errors-spacer { + float: right; + min-height: 24px; + min-width: 1px; } + [dir=rtl] md-input-container .md-errors-spacer { + float: left; } + md-input-container > md-icon { + position: absolute; + top: 8px; + left: 2px; + right: auto; } + [dir=rtl] md-input-container > md-icon { + left: auto; } + [dir=rtl] md-input-container > md-icon { + right: 2px; } + md-input-container textarea, + md-input-container input[type="text"], + md-input-container input[type="password"], + md-input-container input[type="datetime"], + md-input-container input[type="datetime-local"], + md-input-container input[type="date"], + md-input-container input[type="month"], + md-input-container input[type="time"], + md-input-container input[type="week"], + md-input-container input[type="number"], + md-input-container input[type="email"], + md-input-container input[type="url"], + md-input-container input[type="search"], + md-input-container input[type="tel"], + md-input-container input[type="color"] { + /* remove default appearance from all input/textarea */ + -moz-appearance: none; + -webkit-appearance: none; } + md-input-container input[type="date"], + md-input-container input[type="datetime-local"], + md-input-container input[type="month"], + md-input-container input[type="time"], + md-input-container input[type="week"] { + min-height: 26px; } + md-input-container textarea { + resize: none; + overflow: hidden; } + md-input-container textarea.md-input { + min-height: 26px; + -ms-flex-preferred-size: auto; } + md-input-container textarea[md-no-autogrow] { + height: auto; + overflow: auto; } + md-input-container label:not(.md-container-ignore) { + position: absolute; + bottom: 100%; + left: 0; + right: auto; } + [dir=rtl] md-input-container label:not(.md-container-ignore) { + left: auto; } + [dir=rtl] md-input-container label:not(.md-container-ignore) { + right: 0; } + md-input-container label:not(.md-container-ignore).md-required:after { + content: ' *'; + font-size: 13px; + vertical-align: top; } + md-input-container label:not(.md-no-float):not(.md-container-ignore), + md-input-container .md-placeholder { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + width: 100%; + order: 1; + pointer-events: none; + -webkit-font-smoothing: antialiased; + padding-left: 3px; + padding-right: 0; + z-index: 1; + transform: translate3d(0, 28px, 0) scale(1); + transition: transform 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); + max-width: 100%; + transform-origin: left top; } + [dir=rtl] md-input-container label:not(.md-no-float):not(.md-container-ignore), [dir=rtl] + md-input-container .md-placeholder { + padding-left: 0; } + [dir=rtl] md-input-container label:not(.md-no-float):not(.md-container-ignore), [dir=rtl] + md-input-container .md-placeholder { + padding-right: 3px; } + [dir=rtl] md-input-container label:not(.md-no-float):not(.md-container-ignore), [dir=rtl] + md-input-container .md-placeholder { + transform-origin: right top; } + md-input-container .md-placeholder { + position: absolute; + top: 0; + opacity: 0; + transition-property: opacity, transform; + transform: translate3d(0, 30px, 0); } + md-input-container.md-input-focused .md-placeholder { + opacity: 1; + transform: translate3d(0, 24px, 0); } + md-input-container.md-input-has-value .md-placeholder { + transition: none; + opacity: 0; } + md-input-container:not(.md-input-has-value) input:not(:focus), + md-input-container:not(.md-input-has-value) input:not(:focus)::-webkit-datetime-edit-ampm-field, + md-input-container:not(.md-input-has-value) input:not(:focus)::-webkit-datetime-edit-day-field, + md-input-container:not(.md-input-has-value) input:not(:focus)::-webkit-datetime-edit-hour-field, + md-input-container:not(.md-input-has-value) input:not(:focus)::-webkit-datetime-edit-millisecond-field, + md-input-container:not(.md-input-has-value) input:not(:focus)::-webkit-datetime-edit-minute-field, + md-input-container:not(.md-input-has-value) input:not(:focus)::-webkit-datetime-edit-month-field, + md-input-container:not(.md-input-has-value) input:not(:focus)::-webkit-datetime-edit-second-field, + md-input-container:not(.md-input-has-value) input:not(:focus)::-webkit-datetime-edit-week-field, + md-input-container:not(.md-input-has-value) input:not(:focus)::-webkit-datetime-edit-year-field, + md-input-container:not(.md-input-has-value) input:not(:focus)::-webkit-datetime-edit-text { + color: transparent; } + md-input-container .md-input { + order: 2; + display: block; + margin-top: 0; + background: none; + padding-top: 2px; + padding-bottom: 1px; + padding-left: 2px; + padding-right: 2px; + border-width: 0 0 1px 0; + line-height: 26px; + height: 30px; + -ms-flex-preferred-size: 26px; + border-radius: 0; + border-style: solid; + width: 100%; + box-sizing: border-box; + float: left; } + [dir=rtl] md-input-container .md-input { + float: right; } + md-input-container .md-input:focus { + outline: none; } + md-input-container .md-input:invalid { + outline: none; + box-shadow: none; } + md-input-container .md-input.md-no-flex { + flex: none !important; } + md-input-container .md-char-counter { + text-align: right; + padding-right: 2px; + padding-left: 0; } + [dir=rtl] md-input-container .md-char-counter { + text-align: left; } + [dir=rtl] md-input-container .md-char-counter { + padding-right: 0; } + [dir=rtl] md-input-container .md-char-counter { + padding-left: 2px; } + md-input-container .md-input-messages-animation { + position: relative; + order: 4; + overflow: hidden; + clear: left; } + [dir=rtl] md-input-container .md-input-messages-animation { + clear: right; } + md-input-container .md-input-messages-animation.ng-enter .md-input-message-animation { + opacity: 0; + margin-top: -100px; } + md-input-container .md-input-message-animation, md-input-container .md-char-counter { + font-size: 12px; + line-height: 14px; + overflow: hidden; + transition: all 0.3s cubic-bezier(0.55, 0, 0.55, 0.2); + opacity: 1; + margin-top: 0; + padding-top: 5px; } + md-input-container .md-input-message-animation:not(.md-char-counter), md-input-container .md-char-counter:not(.md-char-counter) { + padding-right: 5px; + padding-left: 0; } + [dir=rtl] md-input-container .md-input-message-animation:not(.md-char-counter), [dir=rtl] md-input-container .md-char-counter:not(.md-char-counter) { + padding-right: 0; } + [dir=rtl] md-input-container .md-input-message-animation:not(.md-char-counter), [dir=rtl] md-input-container .md-char-counter:not(.md-char-counter) { + padding-left: 5px; } + md-input-container:not(.md-input-invalid) .md-auto-hide .md-input-message-animation { + opacity: 0; + margin-top: -100px; } + md-input-container .md-input-message-animation:not(.ng-animate) { + opacity: 0; + margin-top: -100px; } + md-input-container .md-input-message-animation.ng-enter { + opacity: 0; + margin-top: -100px; } + md-input-container.md-input-focused label:not(.md-no-float), md-input-container.md-input-has-placeholder label:not(.md-no-float), md-input-container.md-input-has-value label:not(.md-no-float) { + transform: translate3d(0, 6px, 0) scale(0.75); + transition: transform cubic-bezier(0.25, 0.8, 0.25, 1) 0.4s, width cubic-bezier(0.25, 0.8, 0.25, 1) 0.4s; } + md-input-container.md-input-has-value label { + transition: none; } + md-input-container.md-input-focused .md-input, + md-input-container .md-input.ng-invalid.ng-dirty, + md-input-container.md-input-resized .md-input { + padding-bottom: 0; + border-width: 0 0 2px 0; } + md-input-container .md-input[disabled], + [disabled] md-input-container .md-input { + background-position: bottom -1px left 0; + background-size: 4px 1px; + background-repeat: repeat-x; } + md-input-container.md-icon-float { + transition: margin-top 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); } + md-input-container.md-icon-float > label { + pointer-events: none; + position: absolute; } + md-input-container.md-icon-float > md-icon { + top: 8px; + left: 2px; + right: auto; } + [dir=rtl] md-input-container.md-icon-float > md-icon { + left: auto; } + [dir=rtl] md-input-container.md-icon-float > md-icon { + right: 2px; } + md-input-container.md-icon-left > label:not(.md-no-float):not(.md-container-ignore), + md-input-container.md-icon-left > label .md-placeholder, md-input-container.md-icon-right > label:not(.md-no-float):not(.md-container-ignore), + md-input-container.md-icon-right > label .md-placeholder { + width: calc(100% - 36px - 18px); } + md-input-container.md-icon-left { + padding-left: 36px; + padding-right: 0; } + [dir=rtl] md-input-container.md-icon-left { + padding-left: 0; } + [dir=rtl] md-input-container.md-icon-left { + padding-right: 36px; } + md-input-container.md-icon-left > label { + left: 36px; + right: auto; } + [dir=rtl] md-input-container.md-icon-left > label { + left: auto; } + [dir=rtl] md-input-container.md-icon-left > label { + right: 36px; } + md-input-container.md-icon-right { + padding-left: 0; + padding-right: 36px; } + [dir=rtl] md-input-container.md-icon-right { + padding-left: 36px; } + [dir=rtl] md-input-container.md-icon-right { + padding-right: 0; } + md-input-container.md-icon-right > md-icon:last-of-type { + margin: 0; + right: 2px; + left: auto; } + [dir=rtl] md-input-container.md-icon-right > md-icon:last-of-type { + right: auto; } + [dir=rtl] md-input-container.md-icon-right > md-icon:last-of-type { + left: 2px; } + md-input-container.md-icon-left.md-icon-right { + padding-left: 36px; + padding-right: 36px; } + md-input-container.md-icon-left.md-icon-right > label:not(.md-no-float):not(.md-container-ignore), + md-input-container.md-icon-left.md-icon-right > label .md-placeholder { + width: calc(100% - (36px * 2)); } + +.md-resize-wrapper { + position: relative; } + .md-resize-wrapper:after { + content: ''; + display: table; + clear: both; } + +.md-resize-handle { + position: absolute; + bottom: -5px; + left: 0; + height: 10px; + background: transparent; + width: 100%; + cursor: ns-resize; } + +@media screen and (-ms-high-contrast: active) { + md-input-container.md-default-theme > md-icon { + fill: #fff; } } + +md-list { + display: block; + padding: 8px 0px 8px 0px; } + md-list .md-subheader { + font-size: 14px; + font-weight: 500; + letter-spacing: 0.010em; + line-height: 1.2em; } + md-list.md-dense md-list-item, + md-list.md-dense md-list-item .md-list-item-inner { + min-height: 48px; } + md-list.md-dense md-list-item::before, + md-list.md-dense md-list-item .md-list-item-inner::before { + content: ''; + min-height: 48px; + visibility: hidden; + display: inline-block; } + md-list.md-dense md-list-item md-icon:first-child, + md-list.md-dense md-list-item .md-list-item-inner md-icon:first-child { + width: 20px; + height: 20px; } + md-list.md-dense md-list-item > md-icon:first-child:not(.md-avatar-icon), + md-list.md-dense md-list-item .md-list-item-inner > md-icon:first-child:not(.md-avatar-icon) { + margin-right: 36px; } + [dir=rtl] md-list.md-dense md-list-item > md-icon:first-child:not(.md-avatar-icon), [dir=rtl] + md-list.md-dense md-list-item .md-list-item-inner > md-icon:first-child:not(.md-avatar-icon) { + margin-right: auto; + margin-left: 36px; } + md-list.md-dense md-list-item .md-avatar, md-list.md-dense md-list-item .md-avatar-icon, + md-list.md-dense md-list-item .md-list-item-inner .md-avatar, + md-list.md-dense md-list-item .md-list-item-inner .md-avatar-icon { + margin-right: 20px; } + [dir=rtl] md-list.md-dense md-list-item .md-avatar, [dir=rtl] md-list.md-dense md-list-item .md-avatar-icon, [dir=rtl] + md-list.md-dense md-list-item .md-list-item-inner .md-avatar, [dir=rtl] + md-list.md-dense md-list-item .md-list-item-inner .md-avatar-icon { + margin-right: auto; + margin-left: 20px; } + md-list.md-dense md-list-item .md-avatar, + md-list.md-dense md-list-item .md-list-item-inner .md-avatar { + flex: none; + width: 36px; + height: 36px; } + md-list.md-dense md-list-item.md-2-line .md-list-item-text.md-offset, md-list.md-dense md-list-item.md-2-line > .md-no-style .md-list-item-text.md-offset, md-list.md-dense md-list-item.md-3-line .md-list-item-text.md-offset, md-list.md-dense md-list-item.md-3-line > .md-no-style .md-list-item-text.md-offset { + margin-left: 56px; } + [dir=rtl] md-list.md-dense md-list-item.md-2-line .md-list-item-text.md-offset, [dir=rtl] md-list.md-dense md-list-item.md-2-line > .md-no-style .md-list-item-text.md-offset, [dir=rtl] md-list.md-dense md-list-item.md-3-line .md-list-item-text.md-offset, [dir=rtl] md-list.md-dense md-list-item.md-3-line > .md-no-style .md-list-item-text.md-offset { + margin-left: auto; + margin-right: 56px; } + md-list.md-dense md-list-item.md-2-line .md-list-item-text h3, + md-list.md-dense md-list-item.md-2-line .md-list-item-text h4, + md-list.md-dense md-list-item.md-2-line .md-list-item-text p, md-list.md-dense md-list-item.md-2-line > .md-no-style .md-list-item-text h3, + md-list.md-dense md-list-item.md-2-line > .md-no-style .md-list-item-text h4, + md-list.md-dense md-list-item.md-2-line > .md-no-style .md-list-item-text p, md-list.md-dense md-list-item.md-3-line .md-list-item-text h3, + md-list.md-dense md-list-item.md-3-line .md-list-item-text h4, + md-list.md-dense md-list-item.md-3-line .md-list-item-text p, md-list.md-dense md-list-item.md-3-line > .md-no-style .md-list-item-text h3, + md-list.md-dense md-list-item.md-3-line > .md-no-style .md-list-item-text h4, + md-list.md-dense md-list-item.md-3-line > .md-no-style .md-list-item-text p { + line-height: 1.05; + font-size: 12px; } + md-list.md-dense md-list-item.md-2-line .md-list-item-text h3, md-list.md-dense md-list-item.md-2-line > .md-no-style .md-list-item-text h3, md-list.md-dense md-list-item.md-3-line .md-list-item-text h3, md-list.md-dense md-list-item.md-3-line > .md-no-style .md-list-item-text h3 { + font-size: 13px; } + md-list.md-dense md-list-item.md-2-line, md-list.md-dense md-list-item.md-2-line > .md-no-style { + min-height: 60px; } + md-list.md-dense md-list-item.md-2-line::before, md-list.md-dense md-list-item.md-2-line > .md-no-style::before { + content: ''; + min-height: 60px; + visibility: hidden; + display: inline-block; } + md-list.md-dense md-list-item.md-2-line > .md-avatar, md-list.md-dense md-list-item.md-2-line .md-avatar-icon, md-list.md-dense md-list-item.md-2-line > .md-no-style > .md-avatar, md-list.md-dense md-list-item.md-2-line > .md-no-style .md-avatar-icon { + margin-top: 12px; } + md-list.md-dense md-list-item.md-3-line, md-list.md-dense md-list-item.md-3-line > .md-no-style { + min-height: 76px; } + md-list.md-dense md-list-item.md-3-line::before, md-list.md-dense md-list-item.md-3-line > .md-no-style::before { + content: ''; + min-height: 76px; + visibility: hidden; + display: inline-block; } + md-list.md-dense md-list-item.md-3-line > md-icon:first-child, + md-list.md-dense md-list-item.md-3-line > .md-avatar, md-list.md-dense md-list-item.md-3-line > .md-no-style > md-icon:first-child, + md-list.md-dense md-list-item.md-3-line > .md-no-style > .md-avatar { + margin-top: 16px; } + +md-list-item { + position: relative; } + md-list-item.md-proxy-focus.md-focused .md-no-style { + transition: background-color 0.15s linear; } + md-list-item._md-button-wrap { + position: relative; } + md-list-item._md-button-wrap > div.md-button:first-child { + display: flex; + align-items: center; + justify-content: flex-start; + padding: 0 16px; + margin: 0; + font-weight: 400; + text-align: left; + border: medium none; } + [dir=rtl] md-list-item._md-button-wrap > div.md-button:first-child { + text-align: right; } + md-list-item._md-button-wrap > div.md-button:first-child > .md-button:first-child { + position: absolute; + top: 0; + left: 0; + height: 100%; + margin: 0; + padding: 0; } + md-list-item._md-button-wrap > div.md-button:first-child .md-list-item-inner { + width: 100%; + min-height: inherit; } + md-list-item.md-no-proxy, + md-list-item .md-no-style { + position: relative; + padding: 0px 16px; + flex: 1 1 auto; } + md-list-item.md-no-proxy.md-button, + md-list-item .md-no-style.md-button { + font-size: inherit; + height: inherit; + text-align: left; + text-transform: none; + width: 100%; + white-space: normal; + flex-direction: inherit; + align-items: inherit; + border-radius: 0; + margin: 0; } + [dir=rtl] md-list-item.md-no-proxy.md-button, [dir=rtl] + md-list-item .md-no-style.md-button { + text-align: right; } + md-list-item.md-no-proxy.md-button > .md-ripple-container, + md-list-item .md-no-style.md-button > .md-ripple-container { + border-radius: 0; } + md-list-item.md-no-proxy:focus, + md-list-item .md-no-style:focus { + outline: none; } + md-list-item.md-clickable:hover { + cursor: pointer; } + md-list-item md-divider { + position: absolute; + bottom: 0; + left: 0; + width: 100%; } + [dir=rtl] md-list-item md-divider { + left: auto; + right: 0; } + md-list-item md-divider[md-inset] { + left: 72px; + width: calc(100% - 72px); + margin: 0 !important; } + [dir=rtl] md-list-item md-divider[md-inset] { + left: auto; + right: 72px; } + md-list-item, + md-list-item .md-list-item-inner { + display: flex; + justify-content: flex-start; + align-items: center; + min-height: 48px; + height: auto; } + md-list-item::before, + md-list-item .md-list-item-inner::before { + content: ''; + min-height: 48px; + visibility: hidden; + display: inline-block; } + md-list-item > div.md-primary > md-icon:not(.md-avatar-icon), + md-list-item > div.md-secondary > md-icon:not(.md-avatar-icon), + md-list-item > md-icon:first-child:not(.md-avatar-icon), + md-list-item > md-icon.md-secondary:not(.md-avatar-icon), + md-list-item .md-list-item-inner > div.md-primary > md-icon:not(.md-avatar-icon), + md-list-item .md-list-item-inner > div.md-secondary > md-icon:not(.md-avatar-icon), + md-list-item .md-list-item-inner > md-icon:first-child:not(.md-avatar-icon), + md-list-item .md-list-item-inner > md-icon.md-secondary:not(.md-avatar-icon) { + width: 24px; + margin-top: 16px; + margin-bottom: 12px; + box-sizing: content-box; } + md-list-item > div.md-primary > md-checkbox, + md-list-item > div.md-secondary > md-checkbox, + md-list-item > md-checkbox, + md-list-item md-checkbox.md-secondary, + md-list-item .md-list-item-inner > div.md-primary > md-checkbox, + md-list-item .md-list-item-inner > div.md-secondary > md-checkbox, + md-list-item .md-list-item-inner > md-checkbox, + md-list-item .md-list-item-inner md-checkbox.md-secondary { + align-self: center; } + md-list-item > div.md-primary > md-checkbox .md-label, + md-list-item > div.md-secondary > md-checkbox .md-label, + md-list-item > md-checkbox .md-label, + md-list-item md-checkbox.md-secondary .md-label, + md-list-item .md-list-item-inner > div.md-primary > md-checkbox .md-label, + md-list-item .md-list-item-inner > div.md-secondary > md-checkbox .md-label, + md-list-item .md-list-item-inner > md-checkbox .md-label, + md-list-item .md-list-item-inner md-checkbox.md-secondary .md-label { + display: none; } + md-list-item > md-icon:first-child:not(.md-avatar-icon), + md-list-item .md-list-item-inner > md-icon:first-child:not(.md-avatar-icon) { + margin-right: 32px; } + [dir=rtl] md-list-item > md-icon:first-child:not(.md-avatar-icon), [dir=rtl] + md-list-item .md-list-item-inner > md-icon:first-child:not(.md-avatar-icon) { + margin-right: auto; + margin-left: 32px; } + md-list-item .md-avatar, md-list-item .md-avatar-icon, + md-list-item .md-list-item-inner .md-avatar, + md-list-item .md-list-item-inner .md-avatar-icon { + margin-top: 8px; + margin-bottom: 8px; + margin-right: 16px; + border-radius: 50%; + box-sizing: content-box; } + [dir=rtl] md-list-item .md-avatar, [dir=rtl] md-list-item .md-avatar-icon, [dir=rtl] + md-list-item .md-list-item-inner .md-avatar, [dir=rtl] + md-list-item .md-list-item-inner .md-avatar-icon { + margin-right: auto; + margin-left: 16px; } + md-list-item .md-avatar, + md-list-item .md-list-item-inner .md-avatar { + flex: none; + width: 40px; + height: 40px; } + md-list-item .md-avatar-icon, + md-list-item .md-list-item-inner .md-avatar-icon { + padding: 8px; } + md-list-item .md-avatar-icon svg, + md-list-item .md-list-item-inner .md-avatar-icon svg { + width: 24px; + height: 24px; } + md-list-item > md-checkbox, + md-list-item .md-list-item-inner > md-checkbox { + width: 24px; + margin-left: 3px; + margin-right: 29px; + margin-top: 16px; } + [dir=rtl] md-list-item > md-checkbox, [dir=rtl] + md-list-item .md-list-item-inner > md-checkbox { + margin-left: 29px; } + [dir=rtl] md-list-item > md-checkbox, [dir=rtl] + md-list-item .md-list-item-inner > md-checkbox { + margin-right: 3px; } + md-list-item .md-secondary-container, + md-list-item .md-list-item-inner .md-secondary-container { + display: flex; + align-items: center; + flex-shrink: 0; + margin: auto; + margin-right: 0; + margin-left: auto; } + [dir=rtl] md-list-item .md-secondary-container, [dir=rtl] + md-list-item .md-list-item-inner .md-secondary-container { + margin-right: auto; } + [dir=rtl] md-list-item .md-secondary-container, [dir=rtl] + md-list-item .md-list-item-inner .md-secondary-container { + margin-left: 0; } + md-list-item .md-secondary-container .md-button:last-of-type, md-list-item .md-secondary-container .md-icon-button:last-of-type, + md-list-item .md-list-item-inner .md-secondary-container .md-button:last-of-type, + md-list-item .md-list-item-inner .md-secondary-container .md-icon-button:last-of-type { + margin-right: 0; } + [dir=rtl] md-list-item .md-secondary-container .md-button:last-of-type, [dir=rtl] md-list-item .md-secondary-container .md-icon-button:last-of-type, [dir=rtl] + md-list-item .md-list-item-inner .md-secondary-container .md-button:last-of-type, [dir=rtl] + md-list-item .md-list-item-inner .md-secondary-container .md-icon-button:last-of-type { + margin-right: auto; + margin-left: 0; } + md-list-item .md-secondary-container md-checkbox, + md-list-item .md-list-item-inner .md-secondary-container md-checkbox { + margin-top: 0; + margin-bottom: 0; } + md-list-item .md-secondary-container md-checkbox:last-child, + md-list-item .md-list-item-inner .md-secondary-container md-checkbox:last-child { + width: 24px; + margin-right: 0; } + [dir=rtl] md-list-item .md-secondary-container md-checkbox:last-child, [dir=rtl] + md-list-item .md-list-item-inner .md-secondary-container md-checkbox:last-child { + margin-right: auto; + margin-left: 0; } + md-list-item .md-secondary-container md-switch, + md-list-item .md-list-item-inner .md-secondary-container md-switch { + margin-top: 0; + margin-bottom: 0; + margin-right: -6px; } + [dir=rtl] md-list-item .md-secondary-container md-switch, [dir=rtl] + md-list-item .md-list-item-inner .md-secondary-container md-switch { + margin-right: auto; + margin-left: -6px; } + md-list-item > p, md-list-item > .md-list-item-inner > p, + md-list-item .md-list-item-inner > p, + md-list-item .md-list-item-inner > .md-list-item-inner > p { + flex: 1 1 auto; + margin: 0; } + md-list-item.md-2-line, md-list-item.md-2-line > .md-no-style, md-list-item.md-3-line, md-list-item.md-3-line > .md-no-style { + align-items: flex-start; + justify-content: center; } + md-list-item.md-2-line.md-long-text, md-list-item.md-2-line > .md-no-style.md-long-text, md-list-item.md-3-line.md-long-text, md-list-item.md-3-line > .md-no-style.md-long-text { + margin-top: 8px; + margin-bottom: 8px; } + md-list-item.md-2-line .md-list-item-text, md-list-item.md-2-line > .md-no-style .md-list-item-text, md-list-item.md-3-line .md-list-item-text, md-list-item.md-3-line > .md-no-style .md-list-item-text { + flex: 1 1 auto; + margin: auto; + text-overflow: ellipsis; + overflow: hidden; } + md-list-item.md-2-line .md-list-item-text.md-offset, md-list-item.md-2-line > .md-no-style .md-list-item-text.md-offset, md-list-item.md-3-line .md-list-item-text.md-offset, md-list-item.md-3-line > .md-no-style .md-list-item-text.md-offset { + margin-left: 56px; } + [dir=rtl] md-list-item.md-2-line .md-list-item-text.md-offset, [dir=rtl] md-list-item.md-2-line > .md-no-style .md-list-item-text.md-offset, [dir=rtl] md-list-item.md-3-line .md-list-item-text.md-offset, [dir=rtl] md-list-item.md-3-line > .md-no-style .md-list-item-text.md-offset { + margin-left: auto; + margin-right: 56px; } + md-list-item.md-2-line .md-list-item-text h3, md-list-item.md-2-line > .md-no-style .md-list-item-text h3, md-list-item.md-3-line .md-list-item-text h3, md-list-item.md-3-line > .md-no-style .md-list-item-text h3 { + font-size: 16px; + font-weight: 400; + letter-spacing: 0.010em; + margin: 0 0 0px 0; + line-height: 1.2em; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; } + md-list-item.md-2-line .md-list-item-text h4, md-list-item.md-2-line > .md-no-style .md-list-item-text h4, md-list-item.md-3-line .md-list-item-text h4, md-list-item.md-3-line > .md-no-style .md-list-item-text h4 { + font-size: 14px; + letter-spacing: 0.010em; + margin: 3px 0 1px 0; + font-weight: 400; + line-height: 1.2em; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; } + md-list-item.md-2-line .md-list-item-text p, md-list-item.md-2-line > .md-no-style .md-list-item-text p, md-list-item.md-3-line .md-list-item-text p, md-list-item.md-3-line > .md-no-style .md-list-item-text p { + font-size: 14px; + font-weight: 500; + letter-spacing: 0.010em; + margin: 0 0 0 0; + line-height: 1.6em; } + md-list-item.md-2-line, md-list-item.md-2-line > .md-no-style { + height: auto; + min-height: 72px; } + md-list-item.md-2-line::before, md-list-item.md-2-line > .md-no-style::before { + content: ''; + min-height: 72px; + visibility: hidden; + display: inline-block; } + md-list-item.md-2-line > .md-avatar, md-list-item.md-2-line .md-avatar-icon, md-list-item.md-2-line > .md-no-style > .md-avatar, md-list-item.md-2-line > .md-no-style .md-avatar-icon { + margin-top: 12px; } + md-list-item.md-2-line > md-icon:first-child, md-list-item.md-2-line > .md-no-style > md-icon:first-child { + align-self: flex-start; } + md-list-item.md-2-line .md-list-item-text, md-list-item.md-2-line > .md-no-style .md-list-item-text { + flex: 1 1 auto; } + md-list-item.md-3-line, md-list-item.md-3-line > .md-no-style { + height: auto; + min-height: 88px; } + md-list-item.md-3-line::before, md-list-item.md-3-line > .md-no-style::before { + content: ''; + min-height: 88px; + visibility: hidden; + display: inline-block; } + md-list-item.md-3-line > md-icon:first-child, + md-list-item.md-3-line > .md-avatar, md-list-item.md-3-line > .md-no-style > md-icon:first-child, + md-list-item.md-3-line > .md-no-style > .md-avatar { + margin-top: 16px; } + +.md-open-menu-container { + position: fixed; + left: 0; + top: 0; + z-index: 100; + opacity: 0; + border-radius: 2px; } + .md-open-menu-container md-menu-divider { + margin-top: 4px; + margin-bottom: 4px; + height: 1px; + min-height: 1px; + max-height: 1px; + width: 100%; } + .md-open-menu-container md-menu-content > * { + opacity: 0; } + .md-open-menu-container:not(.md-clickable) { + pointer-events: none; } + .md-open-menu-container.md-active { + opacity: 1; + transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); + transition-duration: 200ms; } + .md-open-menu-container.md-active > md-menu-content > * { + opacity: 1; + transition: all 0.3s cubic-bezier(0.55, 0, 0.55, 0.2); + transition-duration: 200ms; + transition-delay: 100ms; } + .md-open-menu-container.md-leave { + opacity: 0; + transition: all 0.3s cubic-bezier(0.55, 0, 0.55, 0.2); + transition-duration: 250ms; } + +md-menu-content { + display: flex; + flex-direction: column; + padding: 8px 0; + max-height: 304px; + overflow-y: auto; } + md-menu-content.md-dense { + max-height: 208px; } + md-menu-content.md-dense md-menu-item { + height: 32px; + min-height: 0px; } + +md-menu-item { + display: flex; + flex-direction: row; + min-height: 48px; + height: 48px; + align-content: center; + justify-content: flex-start; + /* + * We cannot use flex on + * ``` + * + * Now we create the **JavaScript animation** that will trigger the CSS transition: + * + * ```js + * ngModule.animation('.fold-animation', ['$animateCss', function($animateCss) { + * return { + * enter: function(element, doneFn) { + * var height = element[0].offsetHeight; + * return $animateCss(element, { + * from: { height:'0px' }, + * to: { height:height + 'px' }, + * duration: 1 // one second + * }); + * } + * } + * }]); + * ``` + * + * ## More Advanced Uses + * + * `$animateCss` is the underlying code that ngAnimate uses to power **CSS-based animations** behind the scenes. Therefore CSS hooks + * like `.ng-EVENT`, `.ng-EVENT-active`, `.ng-EVENT-stagger` are all features that can be triggered using `$animateCss` via JavaScript code. + * + * This also means that just about any combination of adding classes, removing classes, setting styles, dynamically setting a keyframe animation, + * applying a hardcoded duration or delay value, changing the animation easing or applying a stagger animation are all options that work with + * `$animateCss`. The service itself is smart enough to figure out the combination of options and examine the element styling properties in order + * to provide a working animation that will run in CSS. + * + * The example below showcases a more advanced version of the `.fold-animation` from the example above: + * + * ```js + * ngModule.animation('.fold-animation', ['$animateCss', function($animateCss) { + * return { + * enter: function(element, doneFn) { + * var height = element[0].offsetHeight; + * return $animateCss(element, { + * addClass: 'red large-text pulse-twice', + * easing: 'ease-out', + * from: { height:'0px' }, + * to: { height:height + 'px' }, + * duration: 1 // one second + * }); + * } + * } + * }]); + * ``` + * + * Since we're adding/removing CSS classes then the CSS transition will also pick those up: + * + * ```css + * /* since a hardcoded duration value of 1 was provided in the JavaScript animation code, + * the CSS classes below will be transitioned despite them being defined as regular CSS classes */ + * .red { background:red; } + * .large-text { font-size:20px; } + * + * /* we can also use a keyframe animation and $animateCss will make it work alongside the transition */ + * .pulse-twice { + * animation: 0.5s pulse linear 2; + * -webkit-animation: 0.5s pulse linear 2; + * } + * + * @keyframes pulse { + * from { transform: scale(0.5); } + * to { transform: scale(1.5); } + * } + * + * @-webkit-keyframes pulse { + * from { -webkit-transform: scale(0.5); } + * to { -webkit-transform: scale(1.5); } + * } + * ``` + * + * Given this complex combination of CSS classes, styles and options, `$animateCss` will figure everything out and make the animation happen. + * + * ## How the Options are handled + * + * `$animateCss` is very versatile and intelligent when it comes to figuring out what configurations to apply to the element to ensure the animation + * works with the options provided. Say for example we were adding a class that contained a keyframe value and we wanted to also animate some inline + * styles using the `from` and `to` properties. + * + * ```js + * var animator = $animateCss(element, { + * from: { background:'red' }, + * to: { background:'blue' } + * }); + * animator.start(); + * ``` + * + * ```css + * .rotating-animation { + * animation:0.5s rotate linear; + * -webkit-animation:0.5s rotate linear; + * } + * + * @keyframes rotate { + * from { transform: rotate(0deg); } + * to { transform: rotate(360deg); } + * } + * + * @-webkit-keyframes rotate { + * from { -webkit-transform: rotate(0deg); } + * to { -webkit-transform: rotate(360deg); } + * } + * ``` + * + * The missing pieces here are that we do not have a transition set (within the CSS code nor within the `$animateCss` options) and the duration of the animation is + * going to be detected from what the keyframe styles on the CSS class are. In this event, `$animateCss` will automatically create an inline transition + * style matching the duration detected from the keyframe style (which is present in the CSS class that is being added) and then prepare both the transition + * and keyframe animations to run in parallel on the element. Then when the animation is underway the provided `from` and `to` CSS styles will be applied + * and spread across the transition and keyframe animation. + * + * ## What is returned + * + * `$animateCss` works in two stages: a preparation phase and an animation phase. Therefore when `$animateCss` is first called it will NOT actually + * start the animation. All that is going on here is that the element is being prepared for the animation (which means that the generated CSS classes are + * added and removed on the element). Once `$animateCss` is called it will return an object with the following properties: + * + * ```js + * var animator = $animateCss(element, { ... }); + * ``` + * + * Now what do the contents of our `animator` variable look like: + * + * ```js + * { + * // starts the animation + * start: Function, + * + * // ends (aborts) the animation + * end: Function + * } + * ``` + * + * To actually start the animation we need to run `animation.start()` which will then return a promise that we can hook into to detect when the animation ends. + * If we choose not to run the animation then we MUST run `animation.end()` to perform a cleanup on the element (since some CSS classes and styles may have been + * applied to the element during the preparation phase). Note that all other properties such as duration, delay, transitions and keyframes are just properties + * and that changing them will not reconfigure the parameters of the animation. + * + * ### runner.done() vs runner.then() + * It is documented that `animation.start()` will return a promise object and this is true, however, there is also an additional method available on the + * runner called `.done(callbackFn)`. The done method works the same as `.finally(callbackFn)`, however, it does **not trigger a digest to occur**. + * Therefore, for performance reasons, it's always best to use `runner.done(callback)` instead of `runner.then()`, `runner.catch()` or `runner.finally()` + * unless you really need a digest to kick off afterwards. + * + * Keep in mind that, to make this easier, ngAnimate has tweaked the JS animations API to recognize when a runner instance is returned from $animateCss + * (so there is no need to call `runner.done(doneFn)` inside of your JavaScript animation code). + * Check the {@link ngAnimate.$animateCss#usage animation code above} to see how this works. + * + * @param {DOMElement} element the element that will be animated + * @param {object} options the animation-related options that will be applied during the animation + * + * * `event` - The DOM event (e.g. enter, leave, move). When used, a generated CSS class of `ng-EVENT` and `ng-EVENT-active` will be applied + * to the element during the animation. Multiple events can be provided when spaces are used as a separator. (Note that this will not perform any DOM operation.) + * * `structural` - Indicates that the `ng-` prefix will be added to the event class. Setting to `false` or omitting will turn `ng-EVENT` and + * `ng-EVENT-active` in `EVENT` and `EVENT-active`. Unused if `event` is omitted. + * * `easing` - The CSS easing value that will be applied to the transition or keyframe animation (or both). + * * `transitionStyle` - The raw CSS transition style that will be used (e.g. `1s linear all`). + * * `keyframeStyle` - The raw CSS keyframe animation style that will be used (e.g. `1s my_animation linear`). + * * `from` - The starting CSS styles (a key/value object) that will be applied at the start of the animation. + * * `to` - The ending CSS styles (a key/value object) that will be applied across the animation via a CSS transition. + * * `addClass` - A space separated list of CSS classes that will be added to the element and spread across the animation. + * * `removeClass` - A space separated list of CSS classes that will be removed from the element and spread across the animation. + * * `duration` - A number value representing the total duration of the transition and/or keyframe (note that a value of 1 is 1000ms). If a value of `0` + * is provided then the animation will be skipped entirely. + * * `delay` - A number value representing the total delay of the transition and/or keyframe (note that a value of 1 is 1000ms). If a value of `true` is + * used then whatever delay value is detected from the CSS classes will be mirrored on the elements styles (e.g. by setting delay true then the style value + * of the element will be `transition-delay: DETECTED_VALUE`). Using `true` is useful when you want the CSS classes and inline styles to all share the same + * CSS delay value. + * * `stagger` - A numeric time value representing the delay between successively animated elements + * ({@link ngAnimate#css-staggering-animations Click here to learn how CSS-based staggering works in ngAnimate.}) + * * `staggerIndex` - The numeric index representing the stagger item (e.g. a value of 5 is equal to the sixth item in the stagger; therefore when a + * `stagger` option value of `0.1` is used then there will be a stagger delay of `600ms`) + * * `applyClassesEarly` - Whether or not the classes being added or removed will be used when detecting the animation. This is set by `$animate` when enter/leave/move animations are fired to ensure that the CSS classes are resolved in time. (Note that this will prevent any transitions from occurring on the classes being added and removed.) + * * `cleanupStyles` - Whether or not the provided `from` and `to` styles will be removed once + * the animation is closed. This is useful for when the styles are used purely for the sake of + * the animation and do not have a lasting visual effect on the element (e.g. a collapse and open animation). + * By default this value is set to `false`. + * + * @return {object} an object with start and end methods and details about the animation. + * + * * `start` - The method to start the animation. This will return a `Promise` when called. + * * `end` - This method will cancel the animation and remove all applied CSS classes and styles. + */ +var ONE_SECOND = 1000; +var BASE_TEN = 10; + +var ELAPSED_TIME_MAX_DECIMAL_PLACES = 3; +var CLOSING_TIME_BUFFER = 1.5; + +var DETECT_CSS_PROPERTIES = { + transitionDuration: TRANSITION_DURATION_PROP, + transitionDelay: TRANSITION_DELAY_PROP, + transitionProperty: TRANSITION_PROP + PROPERTY_KEY, + animationDuration: ANIMATION_DURATION_PROP, + animationDelay: ANIMATION_DELAY_PROP, + animationIterationCount: ANIMATION_PROP + ANIMATION_ITERATION_COUNT_KEY +}; + +var DETECT_STAGGER_CSS_PROPERTIES = { + transitionDuration: TRANSITION_DURATION_PROP, + transitionDelay: TRANSITION_DELAY_PROP, + animationDuration: ANIMATION_DURATION_PROP, + animationDelay: ANIMATION_DELAY_PROP +}; + +function getCssKeyframeDurationStyle(duration) { + return [ANIMATION_DURATION_PROP, duration + 's']; +} + +function getCssDelayStyle(delay, isKeyframeAnimation) { + var prop = isKeyframeAnimation ? ANIMATION_DELAY_PROP : TRANSITION_DELAY_PROP; + return [prop, delay + 's']; +} + +function computeCssStyles($window, element, properties) { + var styles = Object.create(null); + var detectedStyles = $window.getComputedStyle(element) || {}; + forEach(properties, function(formalStyleName, actualStyleName) { + var val = detectedStyles[formalStyleName]; + if (val) { + var c = val.charAt(0); + + // only numerical-based values have a negative sign or digit as the first value + if (c === '-' || c === '+' || c >= 0) { + val = parseMaxTime(val); + } + + // by setting this to null in the event that the delay is not set or is set directly as 0 + // then we can still allow for negative values to be used later on and not mistake this + // value for being greater than any other negative value. + if (val === 0) { + val = null; + } + styles[actualStyleName] = val; + } + }); + + return styles; +} + +function parseMaxTime(str) { + var maxValue = 0; + var values = str.split(/\s*,\s*/); + forEach(values, function(value) { + // it's always safe to consider only second values and omit `ms` values since + // getComputedStyle will always handle the conversion for us + if (value.charAt(value.length - 1) == 's') { + value = value.substring(0, value.length - 1); + } + value = parseFloat(value) || 0; + maxValue = maxValue ? Math.max(value, maxValue) : value; + }); + return maxValue; +} + +function truthyTimingValue(val) { + return val === 0 || val != null; +} + +function getCssTransitionDurationStyle(duration, applyOnlyDuration) { + var style = TRANSITION_PROP; + var value = duration + 's'; + if (applyOnlyDuration) { + style += DURATION_KEY; + } else { + value += ' linear all'; + } + return [style, value]; +} + +function createLocalCacheLookup() { + var cache = Object.create(null); + return { + flush: function() { + cache = Object.create(null); + }, + + count: function(key) { + var entry = cache[key]; + return entry ? entry.total : 0; + }, + + get: function(key) { + var entry = cache[key]; + return entry && entry.value; + }, + + put: function(key, value) { + if (!cache[key]) { + cache[key] = { total: 1, value: value }; + } else { + cache[key].total++; + } + } + }; +} + +// we do not reassign an already present style value since +// if we detect the style property value again we may be +// detecting styles that were added via the `from` styles. +// We make use of `isDefined` here since an empty string +// or null value (which is what getPropertyValue will return +// for a non-existing style) will still be marked as a valid +// value for the style (a falsy value implies that the style +// is to be removed at the end of the animation). If we had a simple +// "OR" statement then it would not be enough to catch that. +function registerRestorableStyles(backup, node, properties) { + forEach(properties, function(prop) { + backup[prop] = isDefined(backup[prop]) + ? backup[prop] + : node.style.getPropertyValue(prop); + }); +} + +var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { + var gcsLookup = createLocalCacheLookup(); + var gcsStaggerLookup = createLocalCacheLookup(); + + this.$get = ['$window', '$$jqLite', '$$AnimateRunner', '$timeout', + '$$forceReflow', '$sniffer', '$$rAFScheduler', '$$animateQueue', + function($window, $$jqLite, $$AnimateRunner, $timeout, + $$forceReflow, $sniffer, $$rAFScheduler, $$animateQueue) { + + var applyAnimationClasses = applyAnimationClassesFactory($$jqLite); + + var parentCounter = 0; + function gcsHashFn(node, extraClasses) { + var KEY = "$$ngAnimateParentKey"; + var parentNode = node.parentNode; + var parentID = parentNode[KEY] || (parentNode[KEY] = ++parentCounter); + return parentID + '-' + node.getAttribute('class') + '-' + extraClasses; + } + + function computeCachedCssStyles(node, className, cacheKey, properties) { + var timings = gcsLookup.get(cacheKey); + + if (!timings) { + timings = computeCssStyles($window, node, properties); + if (timings.animationIterationCount === 'infinite') { + timings.animationIterationCount = 1; + } + } + + // we keep putting this in multiple times even though the value and the cacheKey are the same + // because we're keeping an internal tally of how many duplicate animations are detected. + gcsLookup.put(cacheKey, timings); + return timings; + } + + function computeCachedCssStaggerStyles(node, className, cacheKey, properties) { + var stagger; + + // if we have one or more existing matches of matching elements + // containing the same parent + CSS styles (which is how cacheKey works) + // then staggering is possible + if (gcsLookup.count(cacheKey) > 0) { + stagger = gcsStaggerLookup.get(cacheKey); + + if (!stagger) { + var staggerClassName = pendClasses(className, '-stagger'); + + $$jqLite.addClass(node, staggerClassName); + + stagger = computeCssStyles($window, node, properties); + + // force the conversion of a null value to zero incase not set + stagger.animationDuration = Math.max(stagger.animationDuration, 0); + stagger.transitionDuration = Math.max(stagger.transitionDuration, 0); + + $$jqLite.removeClass(node, staggerClassName); + + gcsStaggerLookup.put(cacheKey, stagger); + } + } + + return stagger || {}; + } + + var cancelLastRAFRequest; + var rafWaitQueue = []; + function waitUntilQuiet(callback) { + rafWaitQueue.push(callback); + $$rAFScheduler.waitUntilQuiet(function() { + gcsLookup.flush(); + gcsStaggerLookup.flush(); + + // DO NOT REMOVE THIS LINE OR REFACTOR OUT THE `pageWidth` variable. + // PLEASE EXAMINE THE `$$forceReflow` service to understand why. + var pageWidth = $$forceReflow(); + + // we use a for loop to ensure that if the queue is changed + // during this looping then it will consider new requests + for (var i = 0; i < rafWaitQueue.length; i++) { + rafWaitQueue[i](pageWidth); + } + rafWaitQueue.length = 0; + }); + } + + function computeTimings(node, className, cacheKey) { + var timings = computeCachedCssStyles(node, className, cacheKey, DETECT_CSS_PROPERTIES); + var aD = timings.animationDelay; + var tD = timings.transitionDelay; + timings.maxDelay = aD && tD + ? Math.max(aD, tD) + : (aD || tD); + timings.maxDuration = Math.max( + timings.animationDuration * timings.animationIterationCount, + timings.transitionDuration); + + return timings; + } + + return function init(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 = prepareAnimationOptions(copy(options)); + } + + var restoreStyles = {}; + var node = getDomNode(element); + if (!node + || !node.parentNode + || !$$animateQueue.enabled()) { + return closeAndReturnNoopAnimator(); + } + + var temporaryStyles = []; + var classes = element.attr('class'); + var styles = packageStyles(options); + var animationClosed; + var animationPaused; + var animationCompleted; + var runner; + var runnerHost; + var maxDelay; + var maxDelayTime; + var maxDuration; + var maxDurationTime; + var startTime; + var events = []; + + if (options.duration === 0 || (!$sniffer.animations && !$sniffer.transitions)) { + return closeAndReturnNoopAnimator(); + } + + var method = options.event && isArray(options.event) + ? options.event.join(' ') + : options.event; + + var isStructural = method && options.structural; + var structuralClassName = ''; + var addRemoveClassName = ''; + + if (isStructural) { + structuralClassName = pendClasses(method, EVENT_CLASS_PREFIX, true); + } else if (method) { + structuralClassName = method; + } + + if (options.addClass) { + addRemoveClassName += pendClasses(options.addClass, ADD_CLASS_SUFFIX); + } + + if (options.removeClass) { + if (addRemoveClassName.length) { + addRemoveClassName += ' '; + } + addRemoveClassName += pendClasses(options.removeClass, REMOVE_CLASS_SUFFIX); + } + + // there may be a situation where a structural animation is combined together + // with CSS classes that need to resolve before the animation is computed. + // However this means that there is no explicit CSS code to block the animation + // from happening (by setting 0s none in the class name). If this is the case + // we need to apply the classes before the first rAF so we know to continue if + // there actually is a detected transition or keyframe animation + if (options.applyClassesEarly && addRemoveClassName.length) { + applyAnimationClasses(element, options); + } + + var preparationClasses = [structuralClassName, addRemoveClassName].join(' ').trim(); + var fullClassName = classes + ' ' + preparationClasses; + var activeClasses = pendClasses(preparationClasses, ACTIVE_CLASS_SUFFIX); + var hasToStyles = styles.to && Object.keys(styles.to).length > 0; + var containsKeyframeAnimation = (options.keyframeStyle || '').length > 0; + + // there is no way we can trigger an animation if no styles and + // no classes are being applied which would then trigger a transition, + // unless there a is raw keyframe value that is applied to the element. + if (!containsKeyframeAnimation + && !hasToStyles + && !preparationClasses) { + return closeAndReturnNoopAnimator(); + } + + var cacheKey, stagger; + if (options.stagger > 0) { + var staggerVal = parseFloat(options.stagger); + stagger = { + transitionDelay: staggerVal, + animationDelay: staggerVal, + transitionDuration: 0, + animationDuration: 0 + }; + } else { + cacheKey = gcsHashFn(node, fullClassName); + stagger = computeCachedCssStaggerStyles(node, preparationClasses, cacheKey, DETECT_STAGGER_CSS_PROPERTIES); + } + + if (!options.$$skipPreparationClasses) { + $$jqLite.addClass(element, preparationClasses); + } + + var applyOnlyDuration; + + if (options.transitionStyle) { + var transitionStyle = [TRANSITION_PROP, options.transitionStyle]; + applyInlineStyle(node, transitionStyle); + temporaryStyles.push(transitionStyle); + } + + if (options.duration >= 0) { + applyOnlyDuration = node.style[TRANSITION_PROP].length > 0; + var durationStyle = getCssTransitionDurationStyle(options.duration, applyOnlyDuration); + + // we set the duration so that it will be picked up by getComputedStyle later + applyInlineStyle(node, durationStyle); + temporaryStyles.push(durationStyle); + } + + if (options.keyframeStyle) { + var keyframeStyle = [ANIMATION_PROP, options.keyframeStyle]; + applyInlineStyle(node, keyframeStyle); + temporaryStyles.push(keyframeStyle); + } + + var itemIndex = stagger + ? options.staggerIndex >= 0 + ? options.staggerIndex + : gcsLookup.count(cacheKey) + : 0; + + var isFirst = itemIndex === 0; + + // this is a pre-emptive way of forcing the setup classes to be added and applied INSTANTLY + // without causing any combination of transitions to kick in. By adding a negative delay value + // it forces the setup class' transition to end immediately. We later then remove the negative + // transition delay to allow for the transition to naturally do it's thing. The beauty here is + // that if there is no transition defined then nothing will happen and this will also allow + // other transitions to be stacked on top of each other without any chopping them out. + if (isFirst && !options.skipBlocking) { + blockTransitions(node, SAFE_FAST_FORWARD_DURATION_VALUE); + } + + var timings = computeTimings(node, fullClassName, cacheKey); + var relativeDelay = timings.maxDelay; + maxDelay = Math.max(relativeDelay, 0); + maxDuration = timings.maxDuration; + + var flags = {}; + flags.hasTransitions = timings.transitionDuration > 0; + flags.hasAnimations = timings.animationDuration > 0; + flags.hasTransitionAll = flags.hasTransitions && timings.transitionProperty == 'all'; + flags.applyTransitionDuration = hasToStyles && ( + (flags.hasTransitions && !flags.hasTransitionAll) + || (flags.hasAnimations && !flags.hasTransitions)); + flags.applyAnimationDuration = options.duration && flags.hasAnimations; + flags.applyTransitionDelay = truthyTimingValue(options.delay) && (flags.applyTransitionDuration || flags.hasTransitions); + flags.applyAnimationDelay = truthyTimingValue(options.delay) && flags.hasAnimations; + flags.recalculateTimingStyles = addRemoveClassName.length > 0; + + if (flags.applyTransitionDuration || flags.applyAnimationDuration) { + maxDuration = options.duration ? parseFloat(options.duration) : maxDuration; + + if (flags.applyTransitionDuration) { + flags.hasTransitions = true; + timings.transitionDuration = maxDuration; + applyOnlyDuration = node.style[TRANSITION_PROP + PROPERTY_KEY].length > 0; + temporaryStyles.push(getCssTransitionDurationStyle(maxDuration, applyOnlyDuration)); + } + + if (flags.applyAnimationDuration) { + flags.hasAnimations = true; + timings.animationDuration = maxDuration; + temporaryStyles.push(getCssKeyframeDurationStyle(maxDuration)); + } + } + + if (maxDuration === 0 && !flags.recalculateTimingStyles) { + return closeAndReturnNoopAnimator(); + } + + if (options.delay != null) { + var delayStyle; + if (typeof options.delay !== "boolean") { + delayStyle = parseFloat(options.delay); + // number in options.delay means we have to recalculate the delay for the closing timeout + maxDelay = Math.max(delayStyle, 0); + } + + if (flags.applyTransitionDelay) { + temporaryStyles.push(getCssDelayStyle(delayStyle)); + } + + if (flags.applyAnimationDelay) { + temporaryStyles.push(getCssDelayStyle(delayStyle, true)); + } + } + + // we need to recalculate the delay value since we used a pre-emptive negative + // delay value and the delay value is required for the final event checking. This + // property will ensure that this will happen after the RAF phase has passed. + if (options.duration == null && timings.transitionDuration > 0) { + flags.recalculateTimingStyles = flags.recalculateTimingStyles || isFirst; + } + + maxDelayTime = maxDelay * ONE_SECOND; + maxDurationTime = maxDuration * ONE_SECOND; + if (!options.skipBlocking) { + flags.blockTransition = timings.transitionDuration > 0; + flags.blockKeyframeAnimation = timings.animationDuration > 0 && + stagger.animationDelay > 0 && + stagger.animationDuration === 0; + } + + if (options.from) { + if (options.cleanupStyles) { + registerRestorableStyles(restoreStyles, node, Object.keys(options.from)); + } + applyAnimationFromStyles(element, options); + } + + if (flags.blockTransition || flags.blockKeyframeAnimation) { + applyBlocking(maxDuration); + } else if (!options.skipBlocking) { + blockTransitions(node, false); + } + + // TODO(matsko): for 1.5 change this code to have an animator object for better debugging + return { + $$willAnimate: true, + end: endFn, + start: function() { + if (animationClosed) return; + + runnerHost = { + end: endFn, + cancel: cancelFn, + resume: null, //this will be set during the start() phase + pause: null + }; + + runner = new $$AnimateRunner(runnerHost); + + waitUntilQuiet(start); + + // we don't have access to pause/resume the animation + // since it hasn't run yet. AnimateRunner will therefore + // set noop functions for resume and pause and they will + // later be overridden once the animation is triggered + return runner; + } + }; + + function endFn() { + close(); + } + + function cancelFn() { + close(true); + } + + function close(rejected) { // jshint ignore:line + // if the promise has been called already then we shouldn't close + // the animation again + if (animationClosed || (animationCompleted && animationPaused)) return; + animationClosed = true; + animationPaused = false; + + if (!options.$$skipPreparationClasses) { + $$jqLite.removeClass(element, preparationClasses); + } + $$jqLite.removeClass(element, activeClasses); + + blockKeyframeAnimations(node, false); + blockTransitions(node, false); + + forEach(temporaryStyles, function(entry) { + // There is only one way to remove inline style properties entirely from elements. + // By using `removeProperty` this works, but we need to convert camel-cased CSS + // styles down to hyphenated values. + node.style[entry[0]] = ''; + }); + + applyAnimationClasses(element, options); + applyAnimationStyles(element, options); + + if (Object.keys(restoreStyles).length) { + forEach(restoreStyles, function(value, prop) { + value ? node.style.setProperty(prop, value) + : node.style.removeProperty(prop); + }); + } + + // the reason why we have this option is to allow a synchronous closing callback + // that is fired as SOON as the animation ends (when the CSS is removed) or if + // the animation never takes off at all. A good example is a leave animation since + // the element must be removed just after the animation is over or else the element + // will appear on screen for one animation frame causing an overbearing flicker. + if (options.onDone) { + options.onDone(); + } + + if (events && events.length) { + // Remove the transitionend / animationend listener(s) + element.off(events.join(' '), onAnimationProgress); + } + + //Cancel the fallback closing timeout and remove the timer data + var animationTimerData = element.data(ANIMATE_TIMER_KEY); + if (animationTimerData) { + $timeout.cancel(animationTimerData[0].timer); + element.removeData(ANIMATE_TIMER_KEY); + } + + // if the preparation function fails then the promise is not setup + if (runner) { + runner.complete(!rejected); + } + } + + function applyBlocking(duration) { + if (flags.blockTransition) { + blockTransitions(node, duration); + } + + if (flags.blockKeyframeAnimation) { + blockKeyframeAnimations(node, !!duration); + } + } + + function closeAndReturnNoopAnimator() { + runner = new $$AnimateRunner({ + end: endFn, + cancel: cancelFn + }); + + // should flush the cache animation + waitUntilQuiet(noop); + close(); + + return { + $$willAnimate: false, + start: function() { + return runner; + }, + end: endFn + }; + } + + function onAnimationProgress(event) { + event.stopPropagation(); + var ev = event.originalEvent || event; + + // we now always use `Date.now()` due to the recent changes with + // event.timeStamp in Firefox, Webkit and Chrome (see #13494 for more info) + var timeStamp = ev.$manualTimeStamp || Date.now(); + + /* Firefox (or possibly just Gecko) likes to not round values up + * when a ms measurement is used for the animation */ + var elapsedTime = parseFloat(ev.elapsedTime.toFixed(ELAPSED_TIME_MAX_DECIMAL_PLACES)); + + /* $manualTimeStamp is a mocked timeStamp value which is set + * within browserTrigger(). This is only here so that tests can + * mock animations properly. Real events fallback to event.timeStamp, + * or, if they don't, then a timeStamp is automatically created for them. + * We're checking to see if the timeStamp surpasses the expected delay, + * but we're using elapsedTime instead of the timeStamp on the 2nd + * pre-condition since animationPauseds sometimes close off early */ + if (Math.max(timeStamp - startTime, 0) >= maxDelayTime && elapsedTime >= maxDuration) { + // we set this flag to ensure that if the transition is paused then, when resumed, + // the animation will automatically close itself since transitions cannot be paused. + animationCompleted = true; + close(); + } + } + + function start() { + if (animationClosed) return; + if (!node.parentNode) { + close(); + return; + } + + // even though we only pause keyframe animations here the pause flag + // will still happen when transitions are used. Only the transition will + // not be paused since that is not possible. If the animation ends when + // paused then it will not complete until unpaused or cancelled. + var playPause = function(playAnimation) { + if (!animationCompleted) { + animationPaused = !playAnimation; + if (timings.animationDuration) { + var value = blockKeyframeAnimations(node, animationPaused); + animationPaused + ? temporaryStyles.push(value) + : removeFromArray(temporaryStyles, value); + } + } else if (animationPaused && playAnimation) { + animationPaused = false; + close(); + } + }; + + // checking the stagger duration prevents an accidentally cascade of the CSS delay style + // being inherited from the parent. If the transition duration is zero then we can safely + // rely that the delay value is an intentional stagger delay style. + var maxStagger = itemIndex > 0 + && ((timings.transitionDuration && stagger.transitionDuration === 0) || + (timings.animationDuration && stagger.animationDuration === 0)) + && Math.max(stagger.animationDelay, stagger.transitionDelay); + if (maxStagger) { + $timeout(triggerAnimationStart, + Math.floor(maxStagger * itemIndex * ONE_SECOND), + false); + } else { + triggerAnimationStart(); + } + + // this will decorate the existing promise runner with pause/resume methods + runnerHost.resume = function() { + playPause(true); + }; + + runnerHost.pause = function() { + playPause(false); + }; + + function triggerAnimationStart() { + // just incase a stagger animation kicks in when the animation + // itself was cancelled entirely + if (animationClosed) return; + + applyBlocking(false); + + forEach(temporaryStyles, function(entry) { + var key = entry[0]; + var value = entry[1]; + node.style[key] = value; + }); + + applyAnimationClasses(element, options); + $$jqLite.addClass(element, activeClasses); + + if (flags.recalculateTimingStyles) { + fullClassName = node.className + ' ' + preparationClasses; + cacheKey = gcsHashFn(node, fullClassName); + + timings = computeTimings(node, fullClassName, cacheKey); + relativeDelay = timings.maxDelay; + maxDelay = Math.max(relativeDelay, 0); + maxDuration = timings.maxDuration; + + if (maxDuration === 0) { + close(); + return; + } + + flags.hasTransitions = timings.transitionDuration > 0; + flags.hasAnimations = timings.animationDuration > 0; + } + + if (flags.applyAnimationDelay) { + relativeDelay = typeof options.delay !== "boolean" && truthyTimingValue(options.delay) + ? parseFloat(options.delay) + : relativeDelay; + + maxDelay = Math.max(relativeDelay, 0); + timings.animationDelay = relativeDelay; + delayStyle = getCssDelayStyle(relativeDelay, true); + temporaryStyles.push(delayStyle); + node.style[delayStyle[0]] = delayStyle[1]; + } + + maxDelayTime = maxDelay * ONE_SECOND; + maxDurationTime = maxDuration * ONE_SECOND; + + if (options.easing) { + var easeProp, easeVal = options.easing; + if (flags.hasTransitions) { + easeProp = TRANSITION_PROP + TIMING_KEY; + temporaryStyles.push([easeProp, easeVal]); + node.style[easeProp] = easeVal; + } + if (flags.hasAnimations) { + easeProp = ANIMATION_PROP + TIMING_KEY; + temporaryStyles.push([easeProp, easeVal]); + node.style[easeProp] = easeVal; + } + } + + if (timings.transitionDuration) { + events.push(TRANSITIONEND_EVENT); + } + + if (timings.animationDuration) { + events.push(ANIMATIONEND_EVENT); + } + + startTime = Date.now(); + var timerTime = maxDelayTime + CLOSING_TIME_BUFFER * maxDurationTime; + var endTime = startTime + timerTime; + + var animationsData = element.data(ANIMATE_TIMER_KEY) || []; + var setupFallbackTimer = true; + if (animationsData.length) { + var currentTimerData = animationsData[0]; + setupFallbackTimer = endTime > currentTimerData.expectedEndTime; + if (setupFallbackTimer) { + $timeout.cancel(currentTimerData.timer); + } else { + animationsData.push(close); + } + } + + if (setupFallbackTimer) { + var timer = $timeout(onAnimationExpired, timerTime, false); + animationsData[0] = { + timer: timer, + expectedEndTime: endTime + }; + animationsData.push(close); + element.data(ANIMATE_TIMER_KEY, animationsData); + } + + if (events.length) { + element.on(events.join(' '), onAnimationProgress); + } + + if (options.to) { + if (options.cleanupStyles) { + registerRestorableStyles(restoreStyles, node, Object.keys(options.to)); + } + applyAnimationToStyles(element, options); + } + } + + function onAnimationExpired() { + var animationsData = element.data(ANIMATE_TIMER_KEY); + + // this will be false in the event that the element was + // removed from the DOM (via a leave animation or something + // similar) + if (animationsData) { + for (var i = 1; i < animationsData.length; i++) { + animationsData[i](); + } + element.removeData(ANIMATE_TIMER_KEY); + } + } + } + }; + }]; +}]; + +var $$AnimateCssDriverProvider = ['$$animationProvider', function($$animationProvider) { + $$animationProvider.drivers.push('$$animateCssDriver'); + + var NG_ANIMATE_SHIM_CLASS_NAME = 'ng-animate-shim'; + var NG_ANIMATE_ANCHOR_CLASS_NAME = 'ng-anchor'; + + var NG_OUT_ANCHOR_CLASS_NAME = 'ng-anchor-out'; + var NG_IN_ANCHOR_CLASS_NAME = 'ng-anchor-in'; + + function isDocumentFragment(node) { + return node.parentNode && node.parentNode.nodeType === 11; + } + + this.$get = ['$animateCss', '$rootScope', '$$AnimateRunner', '$rootElement', '$sniffer', '$$jqLite', '$document', + function($animateCss, $rootScope, $$AnimateRunner, $rootElement, $sniffer, $$jqLite, $document) { + + // only browsers that support these properties can render animations + if (!$sniffer.animations && !$sniffer.transitions) return noop; + + var bodyNode = $document[0].body; + var rootNode = getDomNode($rootElement); + + var rootBodyElement = jqLite( + // this is to avoid using something that exists outside of the body + // we also special case the doc fragment case because our unit test code + // appends the $rootElement to the body after the app has been bootstrapped + isDocumentFragment(rootNode) || bodyNode.contains(rootNode) ? rootNode : bodyNode + ); + + var applyAnimationClasses = applyAnimationClassesFactory($$jqLite); + + return function initDriverFn(animationDetails) { + return animationDetails.from && animationDetails.to + ? prepareFromToAnchorAnimation(animationDetails.from, + animationDetails.to, + animationDetails.classes, + animationDetails.anchors) + : prepareRegularAnimation(animationDetails); + }; + + function filterCssClasses(classes) { + //remove all the `ng-` stuff + return classes.replace(/\bng-\S+\b/g, ''); + } + + function getUniqueValues(a, b) { + if (isString(a)) a = a.split(' '); + if (isString(b)) b = b.split(' '); + return a.filter(function(val) { + return b.indexOf(val) === -1; + }).join(' '); + } + + function prepareAnchoredAnimation(classes, outAnchor, inAnchor) { + var clone = jqLite(getDomNode(outAnchor).cloneNode(true)); + var startingClasses = filterCssClasses(getClassVal(clone)); + + outAnchor.addClass(NG_ANIMATE_SHIM_CLASS_NAME); + inAnchor.addClass(NG_ANIMATE_SHIM_CLASS_NAME); + + clone.addClass(NG_ANIMATE_ANCHOR_CLASS_NAME); + + rootBodyElement.append(clone); + + var animatorIn, animatorOut = prepareOutAnimation(); + + // the user may not end up using the `out` animation and + // only making use of the `in` animation or vice-versa. + // In either case we should allow this and not assume the + // animation is over unless both animations are not used. + if (!animatorOut) { + animatorIn = prepareInAnimation(); + if (!animatorIn) { + return end(); + } + } + + var startingAnimator = animatorOut || animatorIn; + + return { + start: function() { + var runner; + + var currentAnimation = startingAnimator.start(); + currentAnimation.done(function() { + currentAnimation = null; + if (!animatorIn) { + animatorIn = prepareInAnimation(); + if (animatorIn) { + currentAnimation = animatorIn.start(); + currentAnimation.done(function() { + currentAnimation = null; + end(); + runner.complete(); + }); + return currentAnimation; + } + } + // in the event that there is no `in` animation + end(); + runner.complete(); + }); + + runner = new $$AnimateRunner({ + end: endFn, + cancel: endFn + }); + + return runner; + + function endFn() { + if (currentAnimation) { + currentAnimation.end(); + } + } + } + }; + + function calculateAnchorStyles(anchor) { + var styles = {}; + + var coords = getDomNode(anchor).getBoundingClientRect(); + + // we iterate directly since safari messes up and doesn't return + // all the keys for the coords object when iterated + forEach(['width','height','top','left'], function(key) { + var value = coords[key]; + switch (key) { + case 'top': + value += bodyNode.scrollTop; + break; + case 'left': + value += bodyNode.scrollLeft; + break; + } + styles[key] = Math.floor(value) + 'px'; + }); + return styles; + } + + function prepareOutAnimation() { + var animator = $animateCss(clone, { + addClass: NG_OUT_ANCHOR_CLASS_NAME, + delay: true, + from: calculateAnchorStyles(outAnchor) + }); + + // read the comment within `prepareRegularAnimation` to understand + // why this check is necessary + return animator.$$willAnimate ? animator : null; + } + + function getClassVal(element) { + return element.attr('class') || ''; + } + + function prepareInAnimation() { + var endingClasses = filterCssClasses(getClassVal(inAnchor)); + var toAdd = getUniqueValues(endingClasses, startingClasses); + var toRemove = getUniqueValues(startingClasses, endingClasses); + + var animator = $animateCss(clone, { + to: calculateAnchorStyles(inAnchor), + addClass: NG_IN_ANCHOR_CLASS_NAME + ' ' + toAdd, + removeClass: NG_OUT_ANCHOR_CLASS_NAME + ' ' + toRemove, + delay: true + }); + + // read the comment within `prepareRegularAnimation` to understand + // why this check is necessary + return animator.$$willAnimate ? animator : null; + } + + function end() { + clone.remove(); + outAnchor.removeClass(NG_ANIMATE_SHIM_CLASS_NAME); + inAnchor.removeClass(NG_ANIMATE_SHIM_CLASS_NAME); + } + } + + function prepareFromToAnchorAnimation(from, to, classes, anchors) { + var fromAnimation = prepareRegularAnimation(from, noop); + var toAnimation = prepareRegularAnimation(to, noop); + + var anchorAnimations = []; + forEach(anchors, function(anchor) { + var outElement = anchor['out']; + var inElement = anchor['in']; + var animator = prepareAnchoredAnimation(classes, outElement, inElement); + if (animator) { + anchorAnimations.push(animator); + } + }); + + // no point in doing anything when there are no elements to animate + if (!fromAnimation && !toAnimation && anchorAnimations.length === 0) return; + + return { + start: function() { + var animationRunners = []; + + if (fromAnimation) { + animationRunners.push(fromAnimation.start()); + } + + if (toAnimation) { + animationRunners.push(toAnimation.start()); + } + + forEach(anchorAnimations, function(animation) { + animationRunners.push(animation.start()); + }); + + var runner = new $$AnimateRunner({ + end: endFn, + cancel: endFn // CSS-driven animations cannot be cancelled, only ended + }); + + $$AnimateRunner.all(animationRunners, function(status) { + runner.complete(status); + }); + + return runner; + + function endFn() { + forEach(animationRunners, function(runner) { + runner.end(); + }); + } + } + }; + } + + function prepareRegularAnimation(animationDetails) { + var element = animationDetails.element; + var options = animationDetails.options || {}; + + if (animationDetails.structural) { + options.event = animationDetails.event; + options.structural = true; + options.applyClassesEarly = true; + + // we special case the leave animation since we want to ensure that + // the element is removed as soon as the animation is over. Otherwise + // a flicker might appear or the element may not be removed at all + if (animationDetails.event === 'leave') { + options.onDone = options.domOperation; + } + } + + // We assign the preparationClasses as the actual animation event since + // the internals of $animateCss will just suffix the event token values + // with `-active` to trigger the animation. + if (options.preparationClasses) { + options.event = concatWithSpace(options.event, options.preparationClasses); + } + + var animator = $animateCss(element, options); + + // the driver lookup code inside of $$animation attempts to spawn a + // driver one by one until a driver returns a.$$willAnimate animator object. + // $animateCss will always return an object, however, it will pass in + // a flag as a hint as to whether an animation was detected or not + return animator.$$willAnimate ? animator : null; + } + }]; +}]; + +// TODO(matsko): use caching here to speed things up for detection +// TODO(matsko): add documentation +// by the time... + +var $$AnimateJsProvider = ['$animateProvider', function($animateProvider) { + this.$get = ['$injector', '$$AnimateRunner', '$$jqLite', + function($injector, $$AnimateRunner, $$jqLite) { + + var applyAnimationClasses = applyAnimationClassesFactory($$jqLite); + // $animateJs(element, 'enter'); + return function(element, event, classes, options) { + var animationClosed = false; + + // the `classes` argument is optional and if it is not used + // then the classes will be resolved from the element's className + // property as well as options.addClass/options.removeClass. + if (arguments.length === 3 && isObject(classes)) { + options = classes; + classes = null; + } + + options = prepareAnimationOptions(options); + if (!classes) { + classes = element.attr('class') || ''; + if (options.addClass) { + classes += ' ' + options.addClass; + } + if (options.removeClass) { + classes += ' ' + options.removeClass; + } + } + + var classesToAdd = options.addClass; + var classesToRemove = options.removeClass; + + // the lookupAnimations function returns a series of animation objects that are + // matched up with one or more of the CSS classes. These animation objects are + // defined via the module.animation factory function. If nothing is detected then + // we don't return anything which then makes $animation query the next driver. + var animations = lookupAnimations(classes); + var before, after; + if (animations.length) { + var afterFn, beforeFn; + if (event == 'leave') { + beforeFn = 'leave'; + afterFn = 'afterLeave'; // TODO(matsko): get rid of this + } else { + beforeFn = 'before' + event.charAt(0).toUpperCase() + event.substr(1); + afterFn = event; + } + + if (event !== 'enter' && event !== 'move') { + before = packageAnimations(element, event, options, animations, beforeFn); + } + after = packageAnimations(element, event, options, animations, afterFn); + } + + // no matching animations + if (!before && !after) return; + + function applyOptions() { + options.domOperation(); + applyAnimationClasses(element, options); + } + + function close() { + animationClosed = true; + applyOptions(); + applyAnimationStyles(element, options); + } + + var runner; + + return { + $$willAnimate: true, + end: function() { + if (runner) { + runner.end(); + } else { + close(); + runner = new $$AnimateRunner(); + runner.complete(true); + } + return runner; + }, + start: function() { + if (runner) { + return runner; + } + + runner = new $$AnimateRunner(); + var closeActiveAnimations; + var chain = []; + + if (before) { + chain.push(function(fn) { + closeActiveAnimations = before(fn); + }); + } + + if (chain.length) { + chain.push(function(fn) { + applyOptions(); + fn(true); + }); + } else { + applyOptions(); + } + + if (after) { + chain.push(function(fn) { + closeActiveAnimations = after(fn); + }); + } + + runner.setHost({ + end: function() { + endAnimations(); + }, + cancel: function() { + endAnimations(true); + } + }); + + $$AnimateRunner.chain(chain, onComplete); + return runner; + + function onComplete(success) { + close(success); + runner.complete(success); + } + + function endAnimations(cancelled) { + if (!animationClosed) { + (closeActiveAnimations || noop)(cancelled); + onComplete(cancelled); + } + } + } + }; + + function executeAnimationFn(fn, element, event, options, onDone) { + var args; + switch (event) { + case 'animate': + args = [element, options.from, options.to, onDone]; + break; + + case 'setClass': + args = [element, classesToAdd, classesToRemove, onDone]; + break; + + case 'addClass': + args = [element, classesToAdd, onDone]; + break; + + case 'removeClass': + args = [element, classesToRemove, onDone]; + break; + + default: + args = [element, onDone]; + break; + } + + args.push(options); + + var value = fn.apply(fn, args); + if (value) { + if (isFunction(value.start)) { + value = value.start(); + } + + if (value instanceof $$AnimateRunner) { + value.done(onDone); + } else if (isFunction(value)) { + // optional onEnd / onCancel callback + return value; + } + } + + return noop; + } + + function groupEventedAnimations(element, event, options, animations, fnName) { + var operations = []; + forEach(animations, function(ani) { + var animation = ani[fnName]; + if (!animation) return; + + // note that all of these animations will run in parallel + operations.push(function() { + var runner; + var endProgressCb; + + var resolved = false; + var onAnimationComplete = function(rejected) { + if (!resolved) { + resolved = true; + (endProgressCb || noop)(rejected); + runner.complete(!rejected); + } + }; + + runner = new $$AnimateRunner({ + end: function() { + onAnimationComplete(); + }, + cancel: function() { + onAnimationComplete(true); + } + }); + + endProgressCb = executeAnimationFn(animation, element, event, options, function(result) { + var cancelled = result === false; + onAnimationComplete(cancelled); + }); + + return runner; + }); + }); + + return operations; + } + + function packageAnimations(element, event, options, animations, fnName) { + var operations = groupEventedAnimations(element, event, options, animations, fnName); + if (operations.length === 0) { + var a,b; + if (fnName === 'beforeSetClass') { + a = groupEventedAnimations(element, 'removeClass', options, animations, 'beforeRemoveClass'); + b = groupEventedAnimations(element, 'addClass', options, animations, 'beforeAddClass'); + } else if (fnName === 'setClass') { + a = groupEventedAnimations(element, 'removeClass', options, animations, 'removeClass'); + b = groupEventedAnimations(element, 'addClass', options, animations, 'addClass'); + } + + if (a) { + operations = operations.concat(a); + } + if (b) { + operations = operations.concat(b); + } + } + + if (operations.length === 0) return; + + // TODO(matsko): add documentation + return function startAnimation(callback) { + var runners = []; + if (operations.length) { + forEach(operations, function(animateFn) { + runners.push(animateFn()); + }); + } + + runners.length ? $$AnimateRunner.all(runners, callback) : callback(); + + return function endFn(reject) { + forEach(runners, function(runner) { + reject ? runner.cancel() : runner.end(); + }); + }; + }; + } + }; + + function lookupAnimations(classes) { + classes = isArray(classes) ? classes : classes.split(' '); + var matches = [], flagMap = {}; + for (var i=0; i < classes.length; i++) { + var klass = classes[i], + animationFactory = $animateProvider.$$registeredAnimations[klass]; + if (animationFactory && !flagMap[klass]) { + matches.push($injector.get(animationFactory)); + flagMap[klass] = true; + } + } + return matches; + } + }]; +}]; + +var $$AnimateJsDriverProvider = ['$$animationProvider', function($$animationProvider) { + $$animationProvider.drivers.push('$$animateJsDriver'); + this.$get = ['$$animateJs', '$$AnimateRunner', function($$animateJs, $$AnimateRunner) { + return function initDriverFn(animationDetails) { + if (animationDetails.from && animationDetails.to) { + var fromAnimation = prepareAnimation(animationDetails.from); + var toAnimation = prepareAnimation(animationDetails.to); + if (!fromAnimation && !toAnimation) return; + + return { + start: function() { + var animationRunners = []; + + if (fromAnimation) { + animationRunners.push(fromAnimation.start()); + } + + if (toAnimation) { + animationRunners.push(toAnimation.start()); + } + + $$AnimateRunner.all(animationRunners, done); + + var runner = new $$AnimateRunner({ + end: endFnFactory(), + cancel: endFnFactory() + }); + + return runner; + + function endFnFactory() { + return function() { + forEach(animationRunners, function(runner) { + // at this point we cannot cancel animations for groups just yet. 1.5+ + runner.end(); + }); + }; + } + + function done(status) { + runner.complete(status); + } + } + }; + } else { + return prepareAnimation(animationDetails); + } + }; + + function prepareAnimation(animationDetails) { + // TODO(matsko): make sure to check for grouped animations and delegate down to normal animations + var element = animationDetails.element; + var event = animationDetails.event; + var options = animationDetails.options; + var classes = animationDetails.classes; + return $$animateJs(element, event, classes, options); + } + }]; +}]; + +var NG_ANIMATE_ATTR_NAME = 'data-ng-animate'; +var NG_ANIMATE_PIN_DATA = '$ngAnimatePin'; +var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { + var PRE_DIGEST_STATE = 1; + var RUNNING_STATE = 2; + var ONE_SPACE = ' '; + + var rules = this.rules = { + skip: [], + cancel: [], + join: [] + }; + + function makeTruthyCssClassMap(classString) { + if (!classString) { + return null; + } + + var keys = classString.split(ONE_SPACE); + var map = Object.create(null); + + forEach(keys, function(key) { + map[key] = true; + }); + return map; + } + + function hasMatchingClasses(newClassString, currentClassString) { + if (newClassString && currentClassString) { + var currentClassMap = makeTruthyCssClassMap(currentClassString); + return newClassString.split(ONE_SPACE).some(function(className) { + return currentClassMap[className]; + }); + } + } + + function isAllowed(ruleType, element, currentAnimation, previousAnimation) { + return rules[ruleType].some(function(fn) { + return fn(element, currentAnimation, previousAnimation); + }); + } + + function hasAnimationClasses(animation, and) { + var a = (animation.addClass || '').length > 0; + var b = (animation.removeClass || '').length > 0; + return and ? a && b : a || b; + } + + rules.join.push(function(element, newAnimation, currentAnimation) { + // if the new animation is class-based then we can just tack that on + return !newAnimation.structural && hasAnimationClasses(newAnimation); + }); + + rules.skip.push(function(element, newAnimation, currentAnimation) { + // there is no need to animate anything if no classes are being added and + // there is no structural animation that will be triggered + return !newAnimation.structural && !hasAnimationClasses(newAnimation); + }); + + rules.skip.push(function(element, newAnimation, currentAnimation) { + // why should we trigger a new structural animation if the element will + // be removed from the DOM anyway? + return currentAnimation.event == 'leave' && newAnimation.structural; + }); + + rules.skip.push(function(element, newAnimation, currentAnimation) { + // if there is an ongoing current animation then don't even bother running the class-based animation + return currentAnimation.structural && currentAnimation.state === RUNNING_STATE && !newAnimation.structural; + }); + + rules.cancel.push(function(element, newAnimation, currentAnimation) { + // there can never be two structural animations running at the same time + return currentAnimation.structural && newAnimation.structural; + }); + + rules.cancel.push(function(element, newAnimation, currentAnimation) { + // if the previous animation is already running, but the new animation will + // be triggered, but the new animation is structural + return currentAnimation.state === RUNNING_STATE && newAnimation.structural; + }); + + rules.cancel.push(function(element, newAnimation, currentAnimation) { + // cancel the animation if classes added / removed in both animation cancel each other out, + // but only if the current animation isn't structural + + if (currentAnimation.structural) return false; + + var nA = newAnimation.addClass; + var nR = newAnimation.removeClass; + var cA = currentAnimation.addClass; + var cR = currentAnimation.removeClass; + + // early detection to save the global CPU shortage :) + if ((isUndefined(nA) && isUndefined(nR)) || (isUndefined(cA) && isUndefined(cR))) { + return false; + } + + return hasMatchingClasses(nA, cR) || hasMatchingClasses(nR, cA); + }); + + this.$get = ['$$rAF', '$rootScope', '$rootElement', '$document', '$$HashMap', + '$$animation', '$$AnimateRunner', '$templateRequest', '$$jqLite', '$$forceReflow', + function($$rAF, $rootScope, $rootElement, $document, $$HashMap, + $$animation, $$AnimateRunner, $templateRequest, $$jqLite, $$forceReflow) { + + var activeAnimationsLookup = new $$HashMap(); + var disabledElementsLookup = new $$HashMap(); + var animationsEnabled = null; + + function postDigestTaskFactory() { + var postDigestCalled = false; + return function(fn) { + // we only issue a call to postDigest before + // it has first passed. This prevents any callbacks + // from not firing once the animation has completed + // since it will be out of the digest cycle. + if (postDigestCalled) { + fn(); + } else { + $rootScope.$$postDigest(function() { + postDigestCalled = true; + fn(); + }); + } + }; + } + + // Wait until all directive and route-related templates are downloaded and + // compiled. The $templateRequest.totalPendingRequests variable keeps track of + // all of the remote templates being currently downloaded. If there are no + // templates currently downloading then the watcher will still fire anyway. + var deregisterWatch = $rootScope.$watch( + function() { return $templateRequest.totalPendingRequests === 0; }, + function(isEmpty) { + if (!isEmpty) return; + deregisterWatch(); + + // Now that all templates have been downloaded, $animate will wait until + // the post digest queue is empty before enabling animations. By having two + // calls to $postDigest calls we can ensure that the flag is enabled at the + // very end of the post digest queue. Since all of the animations in $animate + // use $postDigest, it's important that the code below executes at the end. + // This basically means that the page is fully downloaded and compiled before + // any animations are triggered. + $rootScope.$$postDigest(function() { + $rootScope.$$postDigest(function() { + // we check for null directly in the event that the application already called + // .enabled() with whatever arguments that it provided it with + if (animationsEnabled === null) { + animationsEnabled = true; + } + }); + }); + } + ); + + var callbackRegistry = Object.create(null); + + // remember that the classNameFilter is set during the provider/config + // stage therefore we can optimize here and setup a helper function + var classNameFilter = $animateProvider.classNameFilter(); + var isAnimatableClassName = !classNameFilter + ? function() { return true; } + : function(className) { + return classNameFilter.test(className); + }; + + var applyAnimationClasses = applyAnimationClassesFactory($$jqLite); + + function normalizeAnimationDetails(element, animation) { + return mergeAnimationDetails(element, animation, {}); + } + + // IE9-11 has no method "contains" in SVG element and in Node.prototype. Bug #10259. + var contains = window.Node.prototype.contains || function(arg) { + // jshint bitwise: false + return this === arg || !!(this.compareDocumentPosition(arg) & 16); + // jshint bitwise: true + }; + + function findCallbacks(parent, element, event) { + var targetNode = getDomNode(element); + var targetParentNode = getDomNode(parent); + + var matches = []; + var entries = callbackRegistry[event]; + if (entries) { + forEach(entries, function(entry) { + if (contains.call(entry.node, targetNode)) { + matches.push(entry.callback); + } else if (event === 'leave' && contains.call(entry.node, targetParentNode)) { + matches.push(entry.callback); + } + }); + } + + return matches; + } + + function filterFromRegistry(list, matchContainer, matchCallback) { + var containerNode = extractElementNode(matchContainer); + return list.filter(function(entry) { + var isMatch = entry.node === containerNode && + (!matchCallback || entry.callback === matchCallback); + return !isMatch; + }); + } + + function cleanupEventListeners(phase, element) { + if (phase === 'close' && !element[0].parentNode) { + // If the element is not attached to a parentNode, it has been removed by + // the domOperation, and we can safely remove the event callbacks + $animate.off(element); + } + } + + var $animate = { + on: function(event, container, callback) { + var node = extractElementNode(container); + callbackRegistry[event] = callbackRegistry[event] || []; + callbackRegistry[event].push({ + node: node, + callback: callback + }); + + // Remove the callback when the element is removed from the DOM + jqLite(container).on('$destroy', function() { + var animationDetails = activeAnimationsLookup.get(node); + + if (!animationDetails) { + // If there's an animation ongoing, the callback calling code will remove + // the event listeners. If we'd remove here, the callbacks would be removed + // before the animation ends + $animate.off(event, container, callback); + } + }); + }, + + off: function(event, container, callback) { + if (arguments.length === 1 && !isString(arguments[0])) { + container = arguments[0]; + for (var eventType in callbackRegistry) { + callbackRegistry[eventType] = filterFromRegistry(callbackRegistry[eventType], container); + } + + return; + } + + var entries = callbackRegistry[event]; + if (!entries) return; + + callbackRegistry[event] = arguments.length === 1 + ? null + : filterFromRegistry(entries, container, callback); + }, + + pin: function(element, parentElement) { + assertArg(isElement(element), 'element', 'not an element'); + assertArg(isElement(parentElement), 'parentElement', 'not an element'); + element.data(NG_ANIMATE_PIN_DATA, parentElement); + }, + + push: function(element, event, options, domOperation) { + options = options || {}; + options.domOperation = domOperation; + return queueAnimation(element, event, options); + }, + + // this method has four signatures: + // () - global getter + // (bool) - global setter + // (element) - element getter + // (element, bool) - element setter + enabled: function(element, bool) { + var argCount = arguments.length; + + if (argCount === 0) { + // () - Global getter + bool = !!animationsEnabled; + } else { + var hasElement = isElement(element); + + if (!hasElement) { + // (bool) - Global setter + bool = animationsEnabled = !!element; + } else { + var node = getDomNode(element); + + if (argCount === 1) { + // (element) - Element getter + bool = !disabledElementsLookup.get(node); + } else { + // (element, bool) - Element setter + disabledElementsLookup.put(node, !bool); + } + } + } + + return bool; + } + }; + + return $animate; + + function queueAnimation(element, event, initialOptions) { + // we always make a copy of the options since + // there should never be any side effects on + // the input data when running `$animateCss`. + var options = copy(initialOptions); + + var node, parent; + element = stripCommentsFromElement(element); + if (element) { + node = getDomNode(element); + parent = element.parent(); + } + + options = prepareAnimationOptions(options); + + // we create a fake runner with a working promise. + // These methods will become available after the digest has passed + var runner = new $$AnimateRunner(); + + // this is used to trigger callbacks in postDigest mode + var runInNextPostDigestOrNow = postDigestTaskFactory(); + + if (isArray(options.addClass)) { + options.addClass = options.addClass.join(' '); + } + + if (options.addClass && !isString(options.addClass)) { + options.addClass = null; + } + + if (isArray(options.removeClass)) { + options.removeClass = options.removeClass.join(' '); + } + + if (options.removeClass && !isString(options.removeClass)) { + options.removeClass = null; + } + + if (options.from && !isObject(options.from)) { + options.from = null; + } + + if (options.to && !isObject(options.to)) { + options.to = null; + } + + // there are situations where a directive issues an animation for + // a jqLite wrapper that contains only comment nodes... If this + // happens then there is no way we can perform an animation + if (!node) { + close(); + return runner; + } + + var className = [node.className, options.addClass, options.removeClass].join(' '); + if (!isAnimatableClassName(className)) { + close(); + return runner; + } + + var isStructural = ['enter', 'move', 'leave'].indexOf(event) >= 0; + + var documentHidden = $document[0].hidden; + + // this is a hard disable of all animations for the application or on + // the element itself, therefore there is no need to continue further + // past this point if not enabled + // Animations are also disabled if the document is currently hidden (page is not visible + // to the user), because browsers slow down or do not flush calls to requestAnimationFrame + var skipAnimations = !animationsEnabled || documentHidden || disabledElementsLookup.get(node); + var existingAnimation = (!skipAnimations && activeAnimationsLookup.get(node)) || {}; + var hasExistingAnimation = !!existingAnimation.state; + + // there is no point in traversing the same collection of parent ancestors if a followup + // animation will be run on the same element that already did all that checking work + if (!skipAnimations && (!hasExistingAnimation || existingAnimation.state != PRE_DIGEST_STATE)) { + skipAnimations = !areAnimationsAllowed(element, parent, event); + } + + if (skipAnimations) { + // Callbacks should fire even if the document is hidden (regression fix for issue #14120) + if (documentHidden) notifyProgress(runner, event, 'start'); + close(); + if (documentHidden) notifyProgress(runner, event, 'close'); + return runner; + } + + if (isStructural) { + closeChildAnimations(element); + } + + var newAnimation = { + structural: isStructural, + element: element, + event: event, + addClass: options.addClass, + removeClass: options.removeClass, + close: close, + options: options, + runner: runner + }; + + if (hasExistingAnimation) { + var skipAnimationFlag = isAllowed('skip', element, newAnimation, existingAnimation); + if (skipAnimationFlag) { + if (existingAnimation.state === RUNNING_STATE) { + close(); + return runner; + } else { + mergeAnimationDetails(element, existingAnimation, newAnimation); + return existingAnimation.runner; + } + } + var cancelAnimationFlag = isAllowed('cancel', element, newAnimation, existingAnimation); + if (cancelAnimationFlag) { + if (existingAnimation.state === RUNNING_STATE) { + // this will end the animation right away and it is safe + // to do so since the animation is already running and the + // runner callback code will run in async + existingAnimation.runner.end(); + } else if (existingAnimation.structural) { + // this means that the animation is queued into a digest, but + // hasn't started yet. Therefore it is safe to run the close + // method which will call the runner methods in async. + existingAnimation.close(); + } else { + // this will merge the new animation options into existing animation options + mergeAnimationDetails(element, existingAnimation, newAnimation); + + return existingAnimation.runner; + } + } else { + // a joined animation means that this animation will take over the existing one + // so an example would involve a leave animation taking over an enter. Then when + // the postDigest kicks in the enter will be ignored. + var joinAnimationFlag = isAllowed('join', element, newAnimation, existingAnimation); + if (joinAnimationFlag) { + if (existingAnimation.state === RUNNING_STATE) { + normalizeAnimationDetails(element, newAnimation); + } else { + applyGeneratedPreparationClasses(element, isStructural ? event : null, options); + + event = newAnimation.event = existingAnimation.event; + options = mergeAnimationDetails(element, existingAnimation, newAnimation); + + //we return the same runner since only the option values of this animation will + //be fed into the `existingAnimation`. + return existingAnimation.runner; + } + } + } + } else { + // normalization in this case means that it removes redundant CSS classes that + // already exist (addClass) or do not exist (removeClass) on the element + normalizeAnimationDetails(element, newAnimation); + } + + // when the options are merged and cleaned up we may end up not having to do + // an animation at all, therefore we should check this before issuing a post + // digest callback. Structural animations will always run no matter what. + var isValidAnimation = newAnimation.structural; + if (!isValidAnimation) { + // animate (from/to) can be quickly checked first, otherwise we check if any classes are present + isValidAnimation = (newAnimation.event === 'animate' && Object.keys(newAnimation.options.to || {}).length > 0) + || hasAnimationClasses(newAnimation); + } + + if (!isValidAnimation) { + close(); + clearElementAnimationState(element); + return runner; + } + + // the counter keeps track of cancelled animations + var counter = (existingAnimation.counter || 0) + 1; + newAnimation.counter = counter; + + markElementAnimationState(element, PRE_DIGEST_STATE, newAnimation); + + $rootScope.$$postDigest(function() { + var animationDetails = activeAnimationsLookup.get(node); + var animationCancelled = !animationDetails; + animationDetails = animationDetails || {}; + + // if addClass/removeClass is called before something like enter then the + // registered parent element may not be present. The code below will ensure + // that a final value for parent element is obtained + var parentElement = element.parent() || []; + + // animate/structural/class-based animations all have requirements. Otherwise there + // is no point in performing an animation. The parent node must also be set. + var isValidAnimation = parentElement.length > 0 + && (animationDetails.event === 'animate' + || animationDetails.structural + || hasAnimationClasses(animationDetails)); + + // this means that the previous animation was cancelled + // even if the follow-up animation is the same event + if (animationCancelled || animationDetails.counter !== counter || !isValidAnimation) { + // if another animation did not take over then we need + // to make sure that the domOperation and options are + // handled accordingly + if (animationCancelled) { + applyAnimationClasses(element, options); + applyAnimationStyles(element, options); + } + + // if the event changed from something like enter to leave then we do + // it, otherwise if it's the same then the end result will be the same too + if (animationCancelled || (isStructural && animationDetails.event !== event)) { + options.domOperation(); + runner.end(); + } + + // in the event that the element animation was not cancelled or a follow-up animation + // isn't allowed to animate from here then we need to clear the state of the element + // so that any future animations won't read the expired animation data. + if (!isValidAnimation) { + clearElementAnimationState(element); + } + + return; + } + + // this combined multiple class to addClass / removeClass into a setClass event + // so long as a structural event did not take over the animation + event = !animationDetails.structural && hasAnimationClasses(animationDetails, true) + ? 'setClass' + : animationDetails.event; + + markElementAnimationState(element, RUNNING_STATE); + var realRunner = $$animation(element, event, animationDetails.options); + + // this will update the runner's flow-control events based on + // the `realRunner` object. + runner.setHost(realRunner); + notifyProgress(runner, event, 'start', {}); + + realRunner.done(function(status) { + close(!status); + var animationDetails = activeAnimationsLookup.get(node); + if (animationDetails && animationDetails.counter === counter) { + clearElementAnimationState(getDomNode(element)); + } + notifyProgress(runner, event, 'close', {}); + }); + }); + + return runner; + + function notifyProgress(runner, event, phase, data) { + runInNextPostDigestOrNow(function() { + var callbacks = findCallbacks(parent, element, event); + if (callbacks.length) { + // do not optimize this call here to RAF because + // we don't know how heavy the callback code here will + // be and if this code is buffered then this can + // lead to a performance regression. + $$rAF(function() { + forEach(callbacks, function(callback) { + callback(element, phase, data); + }); + cleanupEventListeners(phase, element); + }); + } else { + cleanupEventListeners(phase, element); + } + }); + runner.progress(event, phase, data); + } + + function close(reject) { // jshint ignore:line + clearGeneratedClasses(element, options); + applyAnimationClasses(element, options); + applyAnimationStyles(element, options); + options.domOperation(); + runner.complete(!reject); + } + } + + function closeChildAnimations(element) { + var node = getDomNode(element); + var children = node.querySelectorAll('[' + NG_ANIMATE_ATTR_NAME + ']'); + forEach(children, function(child) { + var state = parseInt(child.getAttribute(NG_ANIMATE_ATTR_NAME)); + var animationDetails = activeAnimationsLookup.get(child); + if (animationDetails) { + switch (state) { + case RUNNING_STATE: + animationDetails.runner.end(); + /* falls through */ + case PRE_DIGEST_STATE: + activeAnimationsLookup.remove(child); + break; + } + } + }); + } + + function clearElementAnimationState(element) { + var node = getDomNode(element); + node.removeAttribute(NG_ANIMATE_ATTR_NAME); + activeAnimationsLookup.remove(node); + } + + function isMatchingElement(nodeOrElmA, nodeOrElmB) { + return getDomNode(nodeOrElmA) === getDomNode(nodeOrElmB); + } + + /** + * This fn returns false if any of the following is true: + * a) animations on any parent element are disabled, and animations on the element aren't explicitly allowed + * b) a parent element has an ongoing structural animation, and animateChildren is false + * c) the element is not a child of the body + * d) the element is not a child of the $rootElement + */ + function areAnimationsAllowed(element, parentElement, event) { + var bodyElement = jqLite($document[0].body); + var bodyElementDetected = isMatchingElement(element, bodyElement) || element[0].nodeName === 'HTML'; + var rootElementDetected = isMatchingElement(element, $rootElement); + var parentAnimationDetected = false; + var animateChildren; + var elementDisabled = disabledElementsLookup.get(getDomNode(element)); + + var parentHost = jqLite.data(element[0], NG_ANIMATE_PIN_DATA); + if (parentHost) { + parentElement = parentHost; + } + + parentElement = getDomNode(parentElement); + + while (parentElement) { + if (!rootElementDetected) { + // angular doesn't want to attempt to animate elements outside of the application + // therefore we need to ensure that the rootElement is an ancestor of the current element + rootElementDetected = isMatchingElement(parentElement, $rootElement); + } + + if (parentElement.nodeType !== ELEMENT_NODE) { + // no point in inspecting the #document element + break; + } + + var details = activeAnimationsLookup.get(parentElement) || {}; + // either an enter, leave or move animation will commence + // therefore we can't allow any animations to take place + // but if a parent animation is class-based then that's ok + if (!parentAnimationDetected) { + var parentElementDisabled = disabledElementsLookup.get(parentElement); + + if (parentElementDisabled === true && elementDisabled !== false) { + // disable animations if the user hasn't explicitly enabled animations on the + // current element + elementDisabled = true; + // element is disabled via parent element, no need to check anything else + break; + } else if (parentElementDisabled === false) { + elementDisabled = false; + } + parentAnimationDetected = details.structural; + } + + if (isUndefined(animateChildren) || animateChildren === true) { + var value = jqLite.data(parentElement, NG_ANIMATE_CHILDREN_DATA); + if (isDefined(value)) { + animateChildren = value; + } + } + + // there is no need to continue traversing at this point + if (parentAnimationDetected && animateChildren === false) break; + + if (!bodyElementDetected) { + // we also need to ensure that the element is or will be a part of the body element + // otherwise it is pointless to even issue an animation to be rendered + bodyElementDetected = isMatchingElement(parentElement, bodyElement); + } + + if (bodyElementDetected && rootElementDetected) { + // If both body and root have been found, any other checks are pointless, + // as no animation data should live outside the application + break; + } + + if (!rootElementDetected) { + // If no rootElement is detected, check if the parentElement is pinned to another element + parentHost = jqLite.data(parentElement, NG_ANIMATE_PIN_DATA); + if (parentHost) { + // The pin target element becomes the next parent element + parentElement = getDomNode(parentHost); + continue; + } + } + + parentElement = parentElement.parentNode; + } + + var allowAnimation = (!parentAnimationDetected || animateChildren) && elementDisabled !== true; + return allowAnimation && rootElementDetected && bodyElementDetected; + } + + function markElementAnimationState(element, state, details) { + details = details || {}; + details.state = state; + + var node = getDomNode(element); + node.setAttribute(NG_ANIMATE_ATTR_NAME, state); + + var oldValue = activeAnimationsLookup.get(node); + var newValue = oldValue + ? extend(oldValue, details) + : details; + activeAnimationsLookup.put(node, newValue); + } + }]; +}]; + +var $$AnimationProvider = ['$animateProvider', function($animateProvider) { + var NG_ANIMATE_REF_ATTR = 'ng-animate-ref'; + + var drivers = this.drivers = []; + + var RUNNER_STORAGE_KEY = '$$animationRunner'; + + function setRunner(element, runner) { + element.data(RUNNER_STORAGE_KEY, runner); + } + + function removeRunner(element) { + element.removeData(RUNNER_STORAGE_KEY); + } + + function getRunner(element) { + return element.data(RUNNER_STORAGE_KEY); + } + + this.$get = ['$$jqLite', '$rootScope', '$injector', '$$AnimateRunner', '$$HashMap', '$$rAFScheduler', + function($$jqLite, $rootScope, $injector, $$AnimateRunner, $$HashMap, $$rAFScheduler) { + + var animationQueue = []; + var applyAnimationClasses = applyAnimationClassesFactory($$jqLite); + + function sortAnimations(animations) { + var tree = { children: [] }; + var i, lookup = new $$HashMap(); + + // this is done first beforehand so that the hashmap + // is filled with a list of the elements that will be animated + for (i = 0; i < animations.length; i++) { + var animation = animations[i]; + lookup.put(animation.domNode, animations[i] = { + domNode: animation.domNode, + fn: animation.fn, + children: [] + }); + } + + for (i = 0; i < animations.length; i++) { + processNode(animations[i]); + } + + return flatten(tree); + + function processNode(entry) { + if (entry.processed) return entry; + entry.processed = true; + + var elementNode = entry.domNode; + var parentNode = elementNode.parentNode; + lookup.put(elementNode, entry); + + var parentEntry; + while (parentNode) { + parentEntry = lookup.get(parentNode); + if (parentEntry) { + if (!parentEntry.processed) { + parentEntry = processNode(parentEntry); + } + break; + } + parentNode = parentNode.parentNode; + } + + (parentEntry || tree).children.push(entry); + return entry; + } + + function flatten(tree) { + var result = []; + var queue = []; + var i; + + for (i = 0; i < tree.children.length; i++) { + queue.push(tree.children[i]); + } + + var remainingLevelEntries = queue.length; + var nextLevelEntries = 0; + var row = []; + + for (i = 0; i < queue.length; i++) { + var entry = queue[i]; + if (remainingLevelEntries <= 0) { + remainingLevelEntries = nextLevelEntries; + nextLevelEntries = 0; + result.push(row); + row = []; + } + row.push(entry.fn); + entry.children.forEach(function(childEntry) { + nextLevelEntries++; + queue.push(childEntry); + }); + remainingLevelEntries--; + } + + if (row.length) { + result.push(row); + } + + return result; + } + } + + // TODO(matsko): document the signature in a better way + return function(element, event, options) { + options = prepareAnimationOptions(options); + var isStructural = ['enter', 'move', 'leave'].indexOf(event) >= 0; + + // there is no animation at the current moment, however + // these runner methods will get later updated with the + // methods leading into the driver's end/cancel methods + // for now they just stop the animation from starting + var runner = new $$AnimateRunner({ + end: function() { close(); }, + cancel: function() { close(true); } + }); + + if (!drivers.length) { + close(); + return runner; + } + + setRunner(element, runner); + + var classes = mergeClasses(element.attr('class'), mergeClasses(options.addClass, options.removeClass)); + var tempClasses = options.tempClasses; + if (tempClasses) { + classes += ' ' + tempClasses; + options.tempClasses = null; + } + + var prepareClassName; + if (isStructural) { + prepareClassName = 'ng-' + event + PREPARE_CLASS_SUFFIX; + $$jqLite.addClass(element, prepareClassName); + } + + animationQueue.push({ + // this data is used by the postDigest code and passed into + // the driver step function + element: element, + classes: classes, + event: event, + structural: isStructural, + options: options, + beforeStart: beforeStart, + close: close + }); + + element.on('$destroy', handleDestroyedElement); + + // we only want there to be one function called within the post digest + // block. This way we can group animations for all the animations that + // were apart of the same postDigest flush call. + if (animationQueue.length > 1) return runner; + + $rootScope.$$postDigest(function() { + var animations = []; + forEach(animationQueue, function(entry) { + // the element was destroyed early on which removed the runner + // form its storage. This means we can't animate this element + // at all and it already has been closed due to destruction. + if (getRunner(entry.element)) { + animations.push(entry); + } else { + entry.close(); + } + }); + + // now any future animations will be in another postDigest + animationQueue.length = 0; + + var groupedAnimations = groupAnimations(animations); + var toBeSortedAnimations = []; + + forEach(groupedAnimations, function(animationEntry) { + toBeSortedAnimations.push({ + domNode: getDomNode(animationEntry.from ? animationEntry.from.element : animationEntry.element), + fn: function triggerAnimationStart() { + // it's important that we apply the `ng-animate` CSS class and the + // temporary classes before we do any driver invoking since these + // CSS classes may be required for proper CSS detection. + animationEntry.beforeStart(); + + var startAnimationFn, closeFn = animationEntry.close; + + // in the event that the element was removed before the digest runs or + // during the RAF sequencing then we should not trigger the animation. + var targetElement = animationEntry.anchors + ? (animationEntry.from.element || animationEntry.to.element) + : animationEntry.element; + + if (getRunner(targetElement)) { + var operation = invokeFirstDriver(animationEntry); + if (operation) { + startAnimationFn = operation.start; + } + } + + if (!startAnimationFn) { + closeFn(); + } else { + var animationRunner = startAnimationFn(); + animationRunner.done(function(status) { + closeFn(!status); + }); + updateAnimationRunners(animationEntry, animationRunner); + } + } + }); + }); + + // we need to sort each of the animations in order of parent to child + // relationships. This ensures that the child classes are applied at the + // right time. + $$rAFScheduler(sortAnimations(toBeSortedAnimations)); + }); + + return runner; + + // TODO(matsko): change to reference nodes + function getAnchorNodes(node) { + var SELECTOR = '[' + NG_ANIMATE_REF_ATTR + ']'; + var items = node.hasAttribute(NG_ANIMATE_REF_ATTR) + ? [node] + : node.querySelectorAll(SELECTOR); + var anchors = []; + forEach(items, function(node) { + var attr = node.getAttribute(NG_ANIMATE_REF_ATTR); + if (attr && attr.length) { + anchors.push(node); + } + }); + return anchors; + } + + function groupAnimations(animations) { + var preparedAnimations = []; + var refLookup = {}; + forEach(animations, function(animation, index) { + var element = animation.element; + var node = getDomNode(element); + var event = animation.event; + var enterOrMove = ['enter', 'move'].indexOf(event) >= 0; + var anchorNodes = animation.structural ? getAnchorNodes(node) : []; + + if (anchorNodes.length) { + var direction = enterOrMove ? 'to' : 'from'; + + forEach(anchorNodes, function(anchor) { + var key = anchor.getAttribute(NG_ANIMATE_REF_ATTR); + refLookup[key] = refLookup[key] || {}; + refLookup[key][direction] = { + animationID: index, + element: jqLite(anchor) + }; + }); + } else { + preparedAnimations.push(animation); + } + }); + + var usedIndicesLookup = {}; + var anchorGroups = {}; + forEach(refLookup, function(operations, key) { + var from = operations.from; + var to = operations.to; + + if (!from || !to) { + // only one of these is set therefore we can't have an + // anchor animation since all three pieces are required + var index = from ? from.animationID : to.animationID; + var indexKey = index.toString(); + if (!usedIndicesLookup[indexKey]) { + usedIndicesLookup[indexKey] = true; + preparedAnimations.push(animations[index]); + } + return; + } + + var fromAnimation = animations[from.animationID]; + var toAnimation = animations[to.animationID]; + var lookupKey = from.animationID.toString(); + if (!anchorGroups[lookupKey]) { + var group = anchorGroups[lookupKey] = { + structural: true, + beforeStart: function() { + fromAnimation.beforeStart(); + toAnimation.beforeStart(); + }, + close: function() { + fromAnimation.close(); + toAnimation.close(); + }, + classes: cssClassesIntersection(fromAnimation.classes, toAnimation.classes), + from: fromAnimation, + to: toAnimation, + anchors: [] // TODO(matsko): change to reference nodes + }; + + // the anchor animations require that the from and to elements both have at least + // one shared CSS class which effectively marries the two elements together to use + // the same animation driver and to properly sequence the anchor animation. + if (group.classes.length) { + preparedAnimations.push(group); + } else { + preparedAnimations.push(fromAnimation); + preparedAnimations.push(toAnimation); + } + } + + anchorGroups[lookupKey].anchors.push({ + 'out': from.element, 'in': to.element + }); + }); + + return preparedAnimations; + } + + function cssClassesIntersection(a,b) { + a = a.split(' '); + b = b.split(' '); + var matches = []; + + for (var i = 0; i < a.length; i++) { + var aa = a[i]; + if (aa.substring(0,3) === 'ng-') continue; + + for (var j = 0; j < b.length; j++) { + if (aa === b[j]) { + matches.push(aa); + break; + } + } + } + + return matches.join(' '); + } + + function invokeFirstDriver(animationDetails) { + // we loop in reverse order since the more general drivers (like CSS and JS) + // may attempt more elements, but custom drivers are more particular + for (var i = drivers.length - 1; i >= 0; i--) { + var driverName = drivers[i]; + var factory = $injector.get(driverName); + var driver = factory(animationDetails); + if (driver) { + return driver; + } + } + } + + function beforeStart() { + element.addClass(NG_ANIMATE_CLASSNAME); + if (tempClasses) { + $$jqLite.addClass(element, tempClasses); + } + if (prepareClassName) { + $$jqLite.removeClass(element, prepareClassName); + prepareClassName = null; + } + } + + function updateAnimationRunners(animation, newRunner) { + if (animation.from && animation.to) { + update(animation.from.element); + update(animation.to.element); + } else { + update(animation.element); + } + + function update(element) { + var runner = getRunner(element); + if (runner) runner.setHost(newRunner); + } + } + + function handleDestroyedElement() { + var runner = getRunner(element); + if (runner && (event !== 'leave' || !options.$$domOperationFired)) { + runner.end(); + } + } + + function close(rejected) { // jshint ignore:line + element.off('$destroy', handleDestroyedElement); + removeRunner(element); + + applyAnimationClasses(element, options); + applyAnimationStyles(element, options); + options.domOperation(); + + if (tempClasses) { + $$jqLite.removeClass(element, tempClasses); + } + + element.removeClass(NG_ANIMATE_CLASSNAME); + runner.complete(!rejected); + } + }; + }]; +}]; + +/** + * @ngdoc directive + * @name ngAnimateSwap + * @restrict A + * @scope + * + * @description + * + * ngAnimateSwap is a animation-oriented directive that allows for the container to + * be removed and entered in whenever the associated expression changes. A + * common usecase for this directive is a rotating banner or slider component which + * contains one image being present at a time. When the active image changes + * then the old image will perform a `leave` animation and the new element + * will be inserted via an `enter` animation. + * + * @animations + * | Animation | Occurs | + * |----------------------------------|--------------------------------------| + * | {@link ng.$animate#enter enter} | when the new element is inserted to the DOM | + * | {@link ng.$animate#leave leave} | when the old element is removed from the DOM | + * + * @example + * + * + *
+ *
+ * {{ number }} + *
+ *
+ *
+ * + * angular.module('ngAnimateSwapExample', ['ngAnimate']) + * .controller('AppCtrl', ['$scope', '$interval', function($scope, $interval) { + * $scope.number = 0; + * $interval(function() { + * $scope.number++; + * }, 1000); + * + * var colors = ['red','blue','green','yellow','orange']; + * $scope.colorClass = function(number) { + * return colors[number % colors.length]; + * }; + * }]); + * + * + * .container { + * height:250px; + * width:250px; + * position:relative; + * overflow:hidden; + * border:2px solid black; + * } + * .container .cell { + * font-size:150px; + * text-align:center; + * line-height:250px; + * position:absolute; + * top:0; + * left:0; + * right:0; + * border-bottom:2px solid black; + * } + * .swap-animation.ng-enter, .swap-animation.ng-leave { + * transition:0.5s linear all; + * } + * .swap-animation.ng-enter { + * top:-250px; + * } + * .swap-animation.ng-enter-active { + * top:0px; + * } + * .swap-animation.ng-leave { + * top:0px; + * } + * .swap-animation.ng-leave-active { + * top:250px; + * } + * .red { background:red; } + * .green { background:green; } + * .blue { background:blue; } + * .yellow { background:yellow; } + * .orange { background:orange; } + * + *
+ */ +var ngAnimateSwapDirective = ['$animate', '$rootScope', function($animate, $rootScope) { + return { + restrict: 'A', + transclude: 'element', + terminal: true, + priority: 600, // we use 600 here to ensure that the directive is caught before others + link: function(scope, $element, attrs, ctrl, $transclude) { + var previousElement, previousScope; + scope.$watchCollection(attrs.ngAnimateSwap || attrs['for'], function(value) { + if (previousElement) { + $animate.leave(previousElement); + } + if (previousScope) { + previousScope.$destroy(); + previousScope = null; + } + if (value || value === 0) { + previousScope = scope.$new(); + $transclude(previousScope, function(element) { + previousElement = element; + $animate.enter(element, null, $element); + }); + } + }); + } + }; +}]; + +/** + * @ngdoc module + * @name ngAnimate + * @description + * + * The `ngAnimate` module provides support for CSS-based animations (keyframes and transitions) as well as JavaScript-based animations via + * callback hooks. Animations are not enabled by default, however, by including `ngAnimate` the animation hooks are enabled for an Angular app. + * + *
+ * + * # Usage + * Simply put, there are two ways to make use of animations when ngAnimate is used: by using **CSS** and **JavaScript**. The former works purely based + * using CSS (by using matching CSS selectors/styles) and the latter triggers animations that are registered via `module.animation()`. For + * both CSS and JS animations the sole requirement is to have a matching `CSS class` that exists both in the registered animation and within + * the HTML element that the animation will be triggered on. + * + * ## Directive Support + * The following directives are "animation aware": + * + * | Directive | Supported Animations | + * |----------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------| + * | {@link ng.directive:ngRepeat#animations ngRepeat} | enter, leave and move | + * | {@link ngRoute.directive:ngView#animations ngView} | enter and leave | + * | {@link ng.directive:ngInclude#animations ngInclude} | enter and leave | + * | {@link ng.directive:ngSwitch#animations ngSwitch} | enter and leave | + * | {@link ng.directive:ngIf#animations ngIf} | enter and leave | + * | {@link ng.directive:ngClass#animations ngClass} | add and remove (the CSS class(es) present) | + * | {@link ng.directive:ngShow#animations ngShow} & {@link ng.directive:ngHide#animations ngHide} | add and remove (the ng-hide class value) | + * | {@link ng.directive:form#animation-hooks form} & {@link ng.directive:ngModel#animation-hooks ngModel} | add and remove (dirty, pristine, valid, invalid & all other validations) | + * | {@link module:ngMessages#animations ngMessages} | add and remove (ng-active & ng-inactive) | + * | {@link module:ngMessages#animations ngMessage} | enter and leave | + * + * (More information can be found by visiting each the documentation associated with each directive.) + * + * ## CSS-based Animations + * + * CSS-based animations with ngAnimate are unique since they require no JavaScript code at all. By using a CSS class that we reference between our HTML + * and CSS code we can create an animation that will be picked up by Angular when an the underlying directive performs an operation. + * + * The example below shows how an `enter` animation can be made possible on an element using `ng-if`: + * + * ```html + *
+ * Fade me in out + *
+ * + * + * ``` + * + * Notice the CSS class **fade**? We can now create the CSS transition code that references this class: + * + * ```css + * /* The starting CSS styles for the enter animation */ + * .fade.ng-enter { + * transition:0.5s linear all; + * opacity:0; + * } + * + * /* The finishing CSS styles for the enter animation */ + * .fade.ng-enter.ng-enter-active { + * opacity:1; + * } + * ``` + * + * The key thing to remember here is that, depending on the animation event (which each of the directives above trigger depending on what's going on) two + * generated CSS classes will be applied to the element; in the example above we have `.ng-enter` and `.ng-enter-active`. For CSS transitions, the transition + * code **must** be defined within the starting CSS class (in this case `.ng-enter`). The destination class is what the transition will animate towards. + * + * If for example we wanted to create animations for `leave` and `move` (ngRepeat triggers move) then we can do so using the same CSS naming conventions: + * + * ```css + * /* now the element will fade out before it is removed from the DOM */ + * .fade.ng-leave { + * transition:0.5s linear all; + * opacity:1; + * } + * .fade.ng-leave.ng-leave-active { + * opacity:0; + * } + * ``` + * + * We can also make use of **CSS Keyframes** by referencing the keyframe animation within the starting CSS class: + * + * ```css + * /* there is no need to define anything inside of the destination + * CSS class since the keyframe will take charge of the animation */ + * .fade.ng-leave { + * animation: my_fade_animation 0.5s linear; + * -webkit-animation: my_fade_animation 0.5s linear; + * } + * + * @keyframes my_fade_animation { + * from { opacity:1; } + * to { opacity:0; } + * } + * + * @-webkit-keyframes my_fade_animation { + * from { opacity:1; } + * to { opacity:0; } + * } + * ``` + * + * Feel free also mix transitions and keyframes together as well as any other CSS classes on the same element. + * + * ### CSS Class-based Animations + * + * Class-based animations (animations that are triggered via `ngClass`, `ngShow`, `ngHide` and some other directives) have a slightly different + * naming convention. Class-based animations are basic enough that a standard transition or keyframe can be referenced on the class being added + * and removed. + * + * For example if we wanted to do a CSS animation for `ngHide` then we place an animation on the `.ng-hide` CSS class: + * + * ```html + *
+ * Show and hide me + *
+ * + * + * + * ``` + * + * All that is going on here with ngShow/ngHide behind the scenes is the `.ng-hide` class is added/removed (when the hidden state is valid). Since + * ngShow and ngHide are animation aware then we can match up a transition and ngAnimate handles the rest. + * + * In addition the addition and removal of the CSS class, ngAnimate also provides two helper methods that we can use to further decorate the animation + * with CSS styles. + * + * ```html + *
+ * Highlight this box + *
+ * + * + * + * ``` + * + * We can also make use of CSS keyframes by placing them within the CSS classes. + * + * + * ### CSS Staggering Animations + * A Staggering animation is a collection of animations that are issued with a slight delay in between each successive operation resulting in a + * curtain-like effect. The ngAnimate module (versions >=1.2) supports staggering animations and the stagger effect can be + * performed by creating a **ng-EVENT-stagger** CSS class and attaching that class to the base CSS class used for + * the animation. The style property expected within the stagger class can either be a **transition-delay** or an + * **animation-delay** property (or both if your animation contains both transitions and keyframe animations). + * + * ```css + * .my-animation.ng-enter { + * /* standard transition code */ + * transition: 1s linear all; + * opacity:0; + * } + * .my-animation.ng-enter-stagger { + * /* this will have a 100ms delay between each successive leave animation */ + * transition-delay: 0.1s; + * + * /* As of 1.4.4, this must always be set: it signals ngAnimate + * to not accidentally inherit a delay property from another CSS class */ + * transition-duration: 0s; + * } + * .my-animation.ng-enter.ng-enter-active { + * /* standard transition styles */ + * opacity:1; + * } + * ``` + * + * Staggering animations work by default in ngRepeat (so long as the CSS class is defined). Outside of ngRepeat, to use staggering animations + * on your own, they can be triggered by firing multiple calls to the same event on $animate. However, the restrictions surrounding this + * are that each of the elements must have the same CSS className value as well as the same parent element. A stagger operation + * will also be reset if one or more animation frames have passed since the multiple calls to `$animate` were fired. + * + * The following code will issue the **ng-leave-stagger** event on the element provided: + * + * ```js + * var kids = parent.children(); + * + * $animate.leave(kids[0]); //stagger index=0 + * $animate.leave(kids[1]); //stagger index=1 + * $animate.leave(kids[2]); //stagger index=2 + * $animate.leave(kids[3]); //stagger index=3 + * $animate.leave(kids[4]); //stagger index=4 + * + * window.requestAnimationFrame(function() { + * //stagger has reset itself + * $animate.leave(kids[5]); //stagger index=0 + * $animate.leave(kids[6]); //stagger index=1 + * + * $scope.$digest(); + * }); + * ``` + * + * Stagger animations are currently only supported within CSS-defined animations. + * + * ### The `ng-animate` CSS class + * + * When ngAnimate is animating an element it will apply the `ng-animate` CSS class to the element for the duration of the animation. + * This is a temporary CSS class and it will be removed once the animation is over (for both JavaScript and CSS-based animations). + * + * Therefore, animations can be applied to an element using this temporary class directly via CSS. + * + * ```css + * .zipper.ng-animate { + * transition:0.5s linear all; + * } + * .zipper.ng-enter { + * opacity:0; + * } + * .zipper.ng-enter.ng-enter-active { + * opacity:1; + * } + * .zipper.ng-leave { + * opacity:1; + * } + * .zipper.ng-leave.ng-leave-active { + * opacity:0; + * } + * ``` + * + * (Note that the `ng-animate` CSS class is reserved and it cannot be applied on an element directly since ngAnimate will always remove + * the CSS class once an animation has completed.) + * + * + * ### The `ng-[event]-prepare` class + * + * This is a special class that can be used to prevent unwanted flickering / flash of content before + * the actual animation starts. The class is added as soon as an animation is initialized, but removed + * before the actual animation starts (after waiting for a $digest). + * It is also only added for *structural* animations (`enter`, `move`, and `leave`). + * + * In practice, flickering can appear when nesting elements with structural animations such as `ngIf` + * into elements that have class-based animations such as `ngClass`. + * + * ```html + *
+ *
+ *
+ *
+ *
+ * ``` + * + * It is possible that during the `enter` animation, the `.message` div will be briefly visible before it starts animating. + * In that case, you can add styles to the CSS that make sure the element stays hidden before the animation starts: + * + * ```css + * .message.ng-enter-prepare { + * opacity: 0; + * } + * + * ``` + * + * ## JavaScript-based Animations + * + * ngAnimate also allows for animations to be consumed by JavaScript code. The approach is similar to CSS-based animations (where there is a shared + * CSS class that is referenced in our HTML code) but in addition we need to register the JavaScript animation on the module. By making use of the + * `module.animation()` module function we can register the animation. + * + * Let's see an example of a enter/leave animation using `ngRepeat`: + * + * ```html + *
+ * {{ item }} + *
+ * ``` + * + * See the **slide** CSS class? Let's use that class to define an animation that we'll structure in our module code by using `module.animation`: + * + * ```js + * myModule.animation('.slide', [function() { + * return { + * // make note that other events (like addClass/removeClass) + * // have different function input parameters + * enter: function(element, doneFn) { + * jQuery(element).fadeIn(1000, doneFn); + * + * // remember to call doneFn so that angular + * // knows that the animation has concluded + * }, + * + * move: function(element, doneFn) { + * jQuery(element).fadeIn(1000, doneFn); + * }, + * + * leave: function(element, doneFn) { + * jQuery(element).fadeOut(1000, doneFn); + * } + * } + * }]); + * ``` + * + * The nice thing about JS-based animations is that we can inject other services and make use of advanced animation libraries such as + * greensock.js and velocity.js. + * + * If our animation code class-based (meaning that something like `ngClass`, `ngHide` and `ngShow` triggers it) then we can still define + * our animations inside of the same registered animation, however, the function input arguments are a bit different: + * + * ```html + *
+ * this box is moody + *
+ * + * + * + * ``` + * + * ```js + * myModule.animation('.colorful', [function() { + * return { + * addClass: function(element, className, doneFn) { + * // do some cool animation and call the doneFn + * }, + * removeClass: function(element, className, doneFn) { + * // do some cool animation and call the doneFn + * }, + * setClass: function(element, addedClass, removedClass, doneFn) { + * // do some cool animation and call the doneFn + * } + * } + * }]); + * ``` + * + * ## CSS + JS Animations Together + * + * AngularJS 1.4 and higher has taken steps to make the amalgamation of CSS and JS animations more flexible. However, unlike earlier versions of Angular, + * defining CSS and JS animations to work off of the same CSS class will not work anymore. Therefore the example below will only result in **JS animations taking + * charge of the animation**: + * + * ```html + *
+ * Slide in and out + *
+ * ``` + * + * ```js + * myModule.animation('.slide', [function() { + * return { + * enter: function(element, doneFn) { + * jQuery(element).slideIn(1000, doneFn); + * } + * } + * }]); + * ``` + * + * ```css + * .slide.ng-enter { + * transition:0.5s linear all; + * transform:translateY(-100px); + * } + * .slide.ng-enter.ng-enter-active { + * transform:translateY(0); + * } + * ``` + * + * Does this mean that CSS and JS animations cannot be used together? Do JS-based animations always have higher priority? We can make up for the + * lack of CSS animations by using the `$animateCss` service to trigger our own tweaked-out, CSS-based animations directly from + * our own JS-based animation code: + * + * ```js + * myModule.animation('.slide', ['$animateCss', function($animateCss) { + * return { + * enter: function(element) { +* // this will trigger `.slide.ng-enter` and `.slide.ng-enter-active`. + * return $animateCss(element, { + * event: 'enter', + * structural: true + * }); + * } + * } + * }]); + * ``` + * + * The nice thing here is that we can save bandwidth by sticking to our CSS-based animation code and we don't need to rely on a 3rd-party animation framework. + * + * The `$animateCss` service is very powerful since we can feed in all kinds of extra properties that will be evaluated and fed into a CSS transition or + * keyframe animation. For example if we wanted to animate the height of an element while adding and removing classes then we can do so by providing that + * data into `$animateCss` directly: + * + * ```js + * myModule.animation('.slide', ['$animateCss', function($animateCss) { + * return { + * enter: function(element) { + * return $animateCss(element, { + * event: 'enter', + * structural: true, + * addClass: 'maroon-setting', + * from: { height:0 }, + * to: { height: 200 } + * }); + * } + * } + * }]); + * ``` + * + * Now we can fill in the rest via our transition CSS code: + * + * ```css + * /* the transition tells ngAnimate to make the animation happen */ + * .slide.ng-enter { transition:0.5s linear all; } + * + * /* this extra CSS class will be absorbed into the transition + * since the $animateCss code is adding the class */ + * .maroon-setting { background:red; } + * ``` + * + * And `$animateCss` will figure out the rest. Just make sure to have the `done()` callback fire the `doneFn` function to signal when the animation is over. + * + * To learn more about what's possible be sure to visit the {@link ngAnimate.$animateCss $animateCss service}. + * + * ## Animation Anchoring (via `ng-animate-ref`) + * + * ngAnimate in AngularJS 1.4 comes packed with the ability to cross-animate elements between + * structural areas of an application (like views) by pairing up elements using an attribute + * called `ng-animate-ref`. + * + * Let's say for example we have two views that are managed by `ng-view` and we want to show + * that there is a relationship between two components situated in within these views. By using the + * `ng-animate-ref` attribute we can identify that the two components are paired together and we + * can then attach an animation, which is triggered when the view changes. + * + * Say for example we have the following template code: + * + * ```html + * + *
+ *
+ * + * + *
+ * + * + * + * + * + * ``` + * + * Now, when the view changes (once the link is clicked), ngAnimate will examine the + * HTML contents to see if there is a match reference between any components in the view + * that is leaving and the view that is entering. It will scan both the view which is being + * removed (leave) and inserted (enter) to see if there are any paired DOM elements that + * contain a matching ref value. + * + * The two images match since they share the same ref value. ngAnimate will now create a + * transport element (which is a clone of the first image element) and it will then attempt + * to animate to the position of the second image element in the next view. For the animation to + * work a special CSS class called `ng-anchor` will be added to the transported element. + * + * We can now attach a transition onto the `.banner.ng-anchor` CSS class and then + * ngAnimate will handle the entire transition for us as well as the addition and removal of + * any changes of CSS classes between the elements: + * + * ```css + * .banner.ng-anchor { + * /* this animation will last for 1 second since there are + * two phases to the animation (an `in` and an `out` phase) */ + * transition:0.5s linear all; + * } + * ``` + * + * We also **must** include animations for the views that are being entered and removed + * (otherwise anchoring wouldn't be possible since the new view would be inserted right away). + * + * ```css + * .view-animation.ng-enter, .view-animation.ng-leave { + * transition:0.5s linear all; + * position:fixed; + * left:0; + * top:0; + * width:100%; + * } + * .view-animation.ng-enter { + * transform:translateX(100%); + * } + * .view-animation.ng-leave, + * .view-animation.ng-enter.ng-enter-active { + * transform:translateX(0%); + * } + * .view-animation.ng-leave.ng-leave-active { + * transform:translateX(-100%); + * } + * ``` + * + * Now we can jump back to the anchor animation. When the animation happens, there are two stages that occur: + * an `out` and an `in` stage. The `out` stage happens first and that is when the element is animated away + * from its origin. Once that animation is over then the `in` stage occurs which animates the + * element to its destination. The reason why there are two animations is to give enough time + * for the enter animation on the new element to be ready. + * + * The example above sets up a transition for both the in and out phases, but we can also target the out or + * in phases directly via `ng-anchor-out` and `ng-anchor-in`. + * + * ```css + * .banner.ng-anchor-out { + * transition: 0.5s linear all; + * + * /* the scale will be applied during the out animation, + * but will be animated away when the in animation runs */ + * transform: scale(1.2); + * } + * + * .banner.ng-anchor-in { + * transition: 1s linear all; + * } + * ``` + * + * + * + * + * ### Anchoring Demo + * + + + Home +
+
+
+
+
+ + angular.module('anchoringExample', ['ngAnimate', 'ngRoute']) + .config(['$routeProvider', function($routeProvider) { + $routeProvider.when('/', { + templateUrl: 'home.html', + controller: 'HomeController as home' + }); + $routeProvider.when('/profile/:id', { + templateUrl: 'profile.html', + controller: 'ProfileController as profile' + }); + }]) + .run(['$rootScope', function($rootScope) { + $rootScope.records = [ + { id:1, title: "Miss Beulah Roob" }, + { id:2, title: "Trent Morissette" }, + { id:3, title: "Miss Ava Pouros" }, + { id:4, title: "Rod Pouros" }, + { id:5, title: "Abdul Rice" }, + { id:6, title: "Laurie Rutherford Sr." }, + { id:7, title: "Nakia McLaughlin" }, + { id:8, title: "Jordon Blanda DVM" }, + { id:9, title: "Rhoda Hand" }, + { id:10, title: "Alexandrea Sauer" } + ]; + }]) + .controller('HomeController', [function() { + //empty + }]) + .controller('ProfileController', ['$rootScope', '$routeParams', function($rootScope, $routeParams) { + var index = parseInt($routeParams.id, 10); + var record = $rootScope.records[index - 1]; + + this.title = record.title; + this.id = record.id; + }]); + + +

Welcome to the home page

+

Please click on an element

+ + {{ record.title }} + +
+ +
+ {{ profile.title }} +
+
+ + .record { + display:block; + font-size:20px; + } + .profile { + background:black; + color:white; + font-size:100px; + } + .view-container { + position:relative; + } + .view-container > .view.ng-animate { + position:absolute; + top:0; + left:0; + width:100%; + min-height:500px; + } + .view.ng-enter, .view.ng-leave, + .record.ng-anchor { + transition:0.5s linear all; + } + .view.ng-enter { + transform:translateX(100%); + } + .view.ng-enter.ng-enter-active, .view.ng-leave { + transform:translateX(0%); + } + .view.ng-leave.ng-leave-active { + transform:translateX(-100%); + } + .record.ng-anchor-out { + background:red; + } + +
+ * + * ### How is the element transported? + * + * When an anchor animation occurs, ngAnimate will clone the starting element and position it exactly where the starting + * element is located on screen via absolute positioning. The cloned element will be placed inside of the root element + * of the application (where ng-app was defined) and all of the CSS classes of the starting element will be applied. The + * element will then animate into the `out` and `in` animations and will eventually reach the coordinates and match + * the dimensions of the destination element. During the entire animation a CSS class of `.ng-animate-shim` will be applied + * to both the starting and destination elements in order to hide them from being visible (the CSS styling for the class + * is: `visibility:hidden`). Once the anchor reaches its destination then it will be removed and the destination element + * will become visible since the shim class will be removed. + * + * ### How is the morphing handled? + * + * CSS Anchoring relies on transitions and keyframes and the internal code is intelligent enough to figure out + * what CSS classes differ between the starting element and the destination element. These different CSS classes + * will be added/removed on the anchor element and a transition will be applied (the transition that is provided + * in the anchor class). Long story short, ngAnimate will figure out what classes to add and remove which will + * make the transition of the element as smooth and automatic as possible. Be sure to use simple CSS classes that + * do not rely on DOM nesting structure so that the anchor element appears the same as the starting element (since + * the cloned element is placed inside of root element which is likely close to the body element). + * + * Note that if the root element is on the `` element then the cloned node will be placed inside of body. + * + * + * ## Using $animate in your directive code + * + * So far we've explored how to feed in animations into an Angular application, but how do we trigger animations within our own directives in our application? + * By injecting the `$animate` service into our directive code, we can trigger structural and class-based hooks which can then be consumed by animations. Let's + * imagine we have a greeting box that shows and hides itself when the data changes + * + * ```html + * Hi there + * ``` + * + * ```js + * ngModule.directive('greetingBox', ['$animate', function($animate) { + * return function(scope, element, attrs) { + * attrs.$observe('active', function(value) { + * value ? $animate.addClass(element, 'on') : $animate.removeClass(element, 'on'); + * }); + * }); + * }]); + * ``` + * + * Now the `on` CSS class is added and removed on the greeting box component. Now if we add a CSS class on top of the greeting box element + * in our HTML code then we can trigger a CSS or JS animation to happen. + * + * ```css + * /* normally we would create a CSS class to reference on the element */ + * greeting-box.on { transition:0.5s linear all; background:green; color:white; } + * ``` + * + * The `$animate` service contains a variety of other methods like `enter`, `leave`, `animate` and `setClass`. To learn more about what's + * possible be sure to visit the {@link ng.$animate $animate service API page}. + * + * + * ## Callbacks and Promises + * + * When `$animate` is called it returns a promise that can be used to capture when the animation has ended. Therefore if we were to trigger + * an animation (within our directive code) then we can continue performing directive and scope related activities after the animation has + * ended by chaining onto the returned promise that animation method returns. + * + * ```js + * // somewhere within the depths of the directive + * $animate.enter(element, parent).then(function() { + * //the animation has completed + * }); + * ``` + * + * (Note that earlier versions of Angular prior to v1.4 required the promise code to be wrapped using `$scope.$apply(...)`. This is not the case + * anymore.) + * + * In addition to the animation promise, we can also make use of animation-related callbacks within our directives and controller code by registering + * an event listener using the `$animate` service. Let's say for example that an animation was triggered on our view + * routing controller to hook into that: + * + * ```js + * ngModule.controller('HomePageController', ['$animate', function($animate) { + * $animate.on('enter', ngViewElement, function(element) { + * // the animation for this route has completed + * }]); + * }]) + * ``` + * + * (Note that you will need to trigger a digest within the callback to get angular to notice any scope-related changes.) + */ + +var copy; +var extend; +var forEach; +var isArray; +var isDefined; +var isElement; +var isFunction; +var isObject; +var isString; +var isUndefined; +var jqLite; +var noop; + +/** + * @ngdoc service + * @name $animate + * @kind object + * + * @description + * The ngAnimate `$animate` service documentation is the same for the core `$animate` service. + * + * Click here {@link ng.$animate to learn more about animations with `$animate`}. + */ +angular.module('ngAnimate', [], function initAngularHelpers() { + // Access helpers from angular core. + // Do it inside a `config` block to ensure `window.angular` is available. + noop = angular.noop; + copy = angular.copy; + extend = angular.extend; + jqLite = angular.element; + forEach = angular.forEach; + isArray = angular.isArray; + isString = angular.isString; + isObject = angular.isObject; + isUndefined = angular.isUndefined; + isDefined = angular.isDefined; + isFunction = angular.isFunction; + isElement = angular.isElement; +}) + .directive('ngAnimateSwap', ngAnimateSwapDirective) + + .directive('ngAnimateChildren', $$AnimateChildrenDirective) + .factory('$$rAFScheduler', $$rAFSchedulerFactory) + + .provider('$$animateQueue', $$AnimateQueueProvider) + .provider('$$animation', $$AnimationProvider) + + .provider('$animateCss', $AnimateCssProvider) + .provider('$$animateCssDriver', $$AnimateCssDriverProvider) + + .provider('$$animateJs', $$AnimateJsProvider) + .provider('$$animateJsDriver', $$AnimateJsDriverProvider); + + +})(window, window.angular); + +/** + * @license AngularJS v1.5.8 + * (c) 2010-2016 Google, Inc. http://angularjs.org + * License: MIT + */ +(function(window, angular) {'use strict'; + +/** + * @ngdoc module + * @name ngAria + * @description + * + * The `ngAria` module provides support for common + * [ARIA](http://www.w3.org/TR/wai-aria/) + * attributes that convey state or semantic information about the application for users + * of assistive technologies, such as screen readers. + * + *
+ * + * ## Usage + * + * For ngAria to do its magic, simply include the module `ngAria` as a dependency. The following + * directives are supported: + * `ngModel`, `ngChecked`, `ngReadonly`, `ngRequired`, `ngValue`, `ngDisabled`, `ngShow`, `ngHide`, `ngClick`, + * `ngDblClick`, and `ngMessages`. + * + * Below is a more detailed breakdown of the attributes handled by ngAria: + * + * | Directive | Supported Attributes | + * |---------------------------------------------|----------------------------------------------------------------------------------------| + * | {@link ng.directive:ngModel ngModel} | aria-checked, aria-valuemin, aria-valuemax, aria-valuenow, aria-invalid, aria-required, input roles | + * | {@link ng.directive:ngDisabled ngDisabled} | aria-disabled | + * | {@link ng.directive:ngRequired ngRequired} | aria-required + * | {@link ng.directive:ngChecked ngChecked} | aria-checked + * | {@link ng.directive:ngReadonly ngReadonly} | aria-readonly | + * | {@link ng.directive:ngValue ngValue} | aria-checked | + * | {@link ng.directive:ngShow ngShow} | aria-hidden | + * | {@link ng.directive:ngHide ngHide} | aria-hidden | + * | {@link ng.directive:ngDblclick ngDblclick} | tabindex | + * | {@link module:ngMessages ngMessages} | aria-live | + * | {@link ng.directive:ngClick ngClick} | tabindex, keypress event, button role | + * + * Find out more information about each directive by reading the + * {@link guide/accessibility ngAria Developer Guide}. + * + * ## Example + * Using ngDisabled with ngAria: + * ```html + * + * ``` + * Becomes: + * ```html + * + * ``` + * + * ## Disabling Attributes + * It's possible to disable individual attributes added by ngAria with the + * {@link ngAria.$ariaProvider#config config} method. For more details, see the + * {@link guide/accessibility Developer Guide}. + */ + /* global -ngAriaModule */ +var ngAriaModule = angular.module('ngAria', ['ng']). + provider('$aria', $AriaProvider); + +/** +* Internal Utilities +*/ +var nodeBlackList = ['BUTTON', 'A', 'INPUT', 'TEXTAREA', 'SELECT', 'DETAILS', 'SUMMARY']; + +var isNodeOneOf = function(elem, nodeTypeArray) { + if (nodeTypeArray.indexOf(elem[0].nodeName) !== -1) { + return true; + } +}; +/** + * @ngdoc provider + * @name $ariaProvider + * + * @description + * + * Used for configuring the ARIA attributes injected and managed by ngAria. + * + * ```js + * angular.module('myApp', ['ngAria'], function config($ariaProvider) { + * $ariaProvider.config({ + * ariaValue: true, + * tabindex: false + * }); + * }); + *``` + * + * ## Dependencies + * Requires the {@link ngAria} module to be installed. + * + */ +function $AriaProvider() { + var config = { + ariaHidden: true, + ariaChecked: true, + ariaReadonly: true, + ariaDisabled: true, + ariaRequired: true, + ariaInvalid: true, + ariaValue: true, + tabindex: true, + bindKeypress: true, + bindRoleForClick: true + }; + + /** + * @ngdoc method + * @name $ariaProvider#config + * + * @param {object} config object to enable/disable specific ARIA attributes + * + * - **ariaHidden** – `{boolean}` – Enables/disables aria-hidden tags + * - **ariaChecked** – `{boolean}` – Enables/disables aria-checked tags + * - **ariaReadonly** – `{boolean}` – Enables/disables aria-readonly tags + * - **ariaDisabled** – `{boolean}` – Enables/disables aria-disabled tags + * - **ariaRequired** – `{boolean}` – Enables/disables aria-required tags + * - **ariaInvalid** – `{boolean}` – Enables/disables aria-invalid tags + * - **ariaValue** – `{boolean}` – Enables/disables aria-valuemin, aria-valuemax and aria-valuenow tags + * - **tabindex** – `{boolean}` – Enables/disables tabindex tags + * - **bindKeypress** – `{boolean}` – Enables/disables keypress event binding on `div` and + * `li` elements with ng-click + * - **bindRoleForClick** – `{boolean}` – Adds role=button to non-interactive elements like `div` + * using ng-click, making them more accessible to users of assistive technologies + * + * @description + * Enables/disables various ARIA attributes + */ + this.config = function(newConfig) { + config = angular.extend(config, newConfig); + }; + + function watchExpr(attrName, ariaAttr, nodeBlackList, negate) { + return function(scope, elem, attr) { + var ariaCamelName = attr.$normalize(ariaAttr); + if (config[ariaCamelName] && !isNodeOneOf(elem, nodeBlackList) && !attr[ariaCamelName]) { + scope.$watch(attr[attrName], function(boolVal) { + // ensure boolean value + boolVal = negate ? !boolVal : !!boolVal; + elem.attr(ariaAttr, boolVal); + }); + } + }; + } + /** + * @ngdoc service + * @name $aria + * + * @description + * @priority 200 + * + * The $aria service contains helper methods for applying common + * [ARIA](http://www.w3.org/TR/wai-aria/) attributes to HTML directives. + * + * ngAria injects common accessibility attributes that tell assistive technologies when HTML + * elements are enabled, selected, hidden, and more. To see how this is performed with ngAria, + * let's review a code snippet from ngAria itself: + * + *```js + * ngAriaModule.directive('ngDisabled', ['$aria', function($aria) { + * return $aria.$$watchExpr('ngDisabled', 'aria-disabled', nodeBlackList, false); + * }]) + *``` + * Shown above, the ngAria module creates a directive with the same signature as the + * traditional `ng-disabled` directive. But this ngAria version is dedicated to + * solely managing accessibility attributes on custom elements. The internal `$aria` service is + * used to watch the boolean attribute `ngDisabled`. If it has not been explicitly set by the + * developer, `aria-disabled` is injected as an attribute with its value synchronized to the + * value in `ngDisabled`. + * + * Because ngAria hooks into the `ng-disabled` directive, developers do not have to do + * anything to enable this feature. The `aria-disabled` attribute is automatically managed + * simply as a silent side-effect of using `ng-disabled` with the ngAria module. + * + * The full list of directives that interface with ngAria: + * * **ngModel** + * * **ngChecked** + * * **ngReadonly** + * * **ngRequired** + * * **ngDisabled** + * * **ngValue** + * * **ngShow** + * * **ngHide** + * * **ngClick** + * * **ngDblclick** + * * **ngMessages** + * + * Read the {@link guide/accessibility ngAria Developer Guide} for a thorough explanation of each + * directive. + * + * + * ## Dependencies + * Requires the {@link ngAria} module to be installed. + */ + this.$get = function() { + return { + config: function(key) { + return config[key]; + }, + $$watchExpr: watchExpr + }; + }; +} + + +ngAriaModule.directive('ngShow', ['$aria', function($aria) { + return $aria.$$watchExpr('ngShow', 'aria-hidden', [], true); +}]) +.directive('ngHide', ['$aria', function($aria) { + return $aria.$$watchExpr('ngHide', 'aria-hidden', [], false); +}]) +.directive('ngValue', ['$aria', function($aria) { + return $aria.$$watchExpr('ngValue', 'aria-checked', nodeBlackList, false); +}]) +.directive('ngChecked', ['$aria', function($aria) { + return $aria.$$watchExpr('ngChecked', 'aria-checked', nodeBlackList, false); +}]) +.directive('ngReadonly', ['$aria', function($aria) { + return $aria.$$watchExpr('ngReadonly', 'aria-readonly', nodeBlackList, false); +}]) +.directive('ngRequired', ['$aria', function($aria) { + return $aria.$$watchExpr('ngRequired', 'aria-required', nodeBlackList, false); +}]) +.directive('ngModel', ['$aria', function($aria) { + + function shouldAttachAttr(attr, normalizedAttr, elem, allowBlacklistEls) { + return $aria.config(normalizedAttr) && !elem.attr(attr) && (allowBlacklistEls || !isNodeOneOf(elem, nodeBlackList)); + } + + function shouldAttachRole(role, elem) { + // if element does not have role attribute + // AND element type is equal to role (if custom element has a type equaling shape) <-- remove? + // AND element is not INPUT + return !elem.attr('role') && (elem.attr('type') === role) && (elem[0].nodeName !== 'INPUT'); + } + + function getShape(attr, elem) { + var type = attr.type, + role = attr.role; + + return ((type || role) === 'checkbox' || role === 'menuitemcheckbox') ? 'checkbox' : + ((type || role) === 'radio' || role === 'menuitemradio') ? 'radio' : + (type === 'range' || role === 'progressbar' || role === 'slider') ? 'range' : ''; + } + + return { + restrict: 'A', + require: 'ngModel', + priority: 200, //Make sure watches are fired after any other directives that affect the ngModel value + compile: function(elem, attr) { + var shape = getShape(attr, elem); + + return { + pre: function(scope, elem, attr, ngModel) { + if (shape === 'checkbox') { + //Use the input[checkbox] $isEmpty implementation for elements with checkbox roles + ngModel.$isEmpty = function(value) { + return value === false; + }; + } + }, + post: function(scope, elem, attr, ngModel) { + var needsTabIndex = shouldAttachAttr('tabindex', 'tabindex', elem, false); + + function ngAriaWatchModelValue() { + return ngModel.$modelValue; + } + + function getRadioReaction(newVal) { + var boolVal = (attr.value == ngModel.$viewValue); + elem.attr('aria-checked', boolVal); + } + + function getCheckboxReaction() { + elem.attr('aria-checked', !ngModel.$isEmpty(ngModel.$viewValue)); + } + + switch (shape) { + case 'radio': + case 'checkbox': + if (shouldAttachRole(shape, elem)) { + elem.attr('role', shape); + } + if (shouldAttachAttr('aria-checked', 'ariaChecked', elem, false)) { + scope.$watch(ngAriaWatchModelValue, shape === 'radio' ? + getRadioReaction : getCheckboxReaction); + } + if (needsTabIndex) { + elem.attr('tabindex', 0); + } + break; + case 'range': + if (shouldAttachRole(shape, elem)) { + elem.attr('role', 'slider'); + } + if ($aria.config('ariaValue')) { + var needsAriaValuemin = !elem.attr('aria-valuemin') && + (attr.hasOwnProperty('min') || attr.hasOwnProperty('ngMin')); + var needsAriaValuemax = !elem.attr('aria-valuemax') && + (attr.hasOwnProperty('max') || attr.hasOwnProperty('ngMax')); + var needsAriaValuenow = !elem.attr('aria-valuenow'); + + if (needsAriaValuemin) { + attr.$observe('min', function ngAriaValueMinReaction(newVal) { + elem.attr('aria-valuemin', newVal); + }); + } + if (needsAriaValuemax) { + attr.$observe('max', function ngAriaValueMinReaction(newVal) { + elem.attr('aria-valuemax', newVal); + }); + } + if (needsAriaValuenow) { + scope.$watch(ngAriaWatchModelValue, function ngAriaValueNowReaction(newVal) { + elem.attr('aria-valuenow', newVal); + }); + } + } + if (needsTabIndex) { + elem.attr('tabindex', 0); + } + break; + } + + if (!attr.hasOwnProperty('ngRequired') && ngModel.$validators.required + && shouldAttachAttr('aria-required', 'ariaRequired', elem, false)) { + // ngModel.$error.required is undefined on custom controls + attr.$observe('required', function() { + elem.attr('aria-required', !!attr['required']); + }); + } + + if (shouldAttachAttr('aria-invalid', 'ariaInvalid', elem, true)) { + scope.$watch(function ngAriaInvalidWatch() { + return ngModel.$invalid; + }, function ngAriaInvalidReaction(newVal) { + elem.attr('aria-invalid', !!newVal); + }); + } + } + }; + } + }; +}]) +.directive('ngDisabled', ['$aria', function($aria) { + return $aria.$$watchExpr('ngDisabled', 'aria-disabled', nodeBlackList, false); +}]) +.directive('ngMessages', function() { + return { + restrict: 'A', + require: '?ngMessages', + link: function(scope, elem, attr, ngMessages) { + if (!elem.attr('aria-live')) { + elem.attr('aria-live', 'assertive'); + } + } + }; +}) +.directive('ngClick',['$aria', '$parse', function($aria, $parse) { + return { + restrict: 'A', + compile: function(elem, attr) { + var fn = $parse(attr.ngClick, /* interceptorFn */ null, /* expensiveChecks */ true); + return function(scope, elem, attr) { + + if (!isNodeOneOf(elem, nodeBlackList)) { + + if ($aria.config('bindRoleForClick') && !elem.attr('role')) { + elem.attr('role', 'button'); + } + + if ($aria.config('tabindex') && !elem.attr('tabindex')) { + elem.attr('tabindex', 0); + } + + if ($aria.config('bindKeypress') && !attr.ngKeypress) { + elem.on('keypress', function(event) { + var keyCode = event.which || event.keyCode; + if (keyCode === 32 || keyCode === 13) { + scope.$apply(callback); + } + + function callback() { + fn(scope, { $event: event }); + } + }); + } + } + }; + } + }; +}]) +.directive('ngDblclick', ['$aria', function($aria) { + return function(scope, elem, attr) { + if ($aria.config('tabindex') && !elem.attr('tabindex') && !isNodeOneOf(elem, nodeBlackList)) { + elem.attr('tabindex', 0); + } + }; +}]); + + +})(window, window.angular); + + +(function (factory) { + 'use strict'; + if (typeof exports === 'object') { + // Node/CommonJS + module.exports = factory( + typeof angular !== 'undefined' ? angular : require('angular'), + typeof Chart !== 'undefined' ? Chart : require('chart.js')); + } else if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(['angular', 'chart'], factory); + } else { + // Browser globals + factory(angular, Chart); + } +}(function (angular, Chart) { + 'use strict'; + + Chart.defaults.global.responsive = true; + Chart.defaults.global.multiTooltipTemplate = '<%if (datasetLabel){%><%=datasetLabel%>: <%}%><%= value %>'; + + Chart.defaults.global.colours = [ + '#97BBCD', // blue + '#DCDCDC', // light grey + '#F7464A', // red + '#46BFBD', // green + '#FDB45C', // yellow + '#949FB1', // grey + '#4D5360' // dark grey + ]; + + var usingExcanvas = typeof window.G_vmlCanvasManager === 'object' && + window.G_vmlCanvasManager !== null && + typeof window.G_vmlCanvasManager.initElement === 'function'; + + if (usingExcanvas) Chart.defaults.global.animation = false; + + return angular.module('chart.js', []) + .provider('ChartJs', ChartJsProvider) + .factory('ChartJsFactory', ['ChartJs', '$timeout', ChartJsFactory]) + .directive('chartBase', ['ChartJsFactory', function (ChartJsFactory) { return new ChartJsFactory(); }]) + .directive('chartLine', ['ChartJsFactory', function (ChartJsFactory) { return new ChartJsFactory('Line'); }]) + .directive('chartBar', ['ChartJsFactory', function (ChartJsFactory) { return new ChartJsFactory('Bar'); }]) + .directive('chartRadar', ['ChartJsFactory', function (ChartJsFactory) { return new ChartJsFactory('Radar'); }]) + .directive('chartDoughnut', ['ChartJsFactory', function (ChartJsFactory) { return new ChartJsFactory('Doughnut'); }]) + .directive('chartPie', ['ChartJsFactory', function (ChartJsFactory) { return new ChartJsFactory('Pie'); }]) + .directive('chartPolarArea', ['ChartJsFactory', function (ChartJsFactory) { return new ChartJsFactory('PolarArea'); }]); + + /** + * Wrapper for chart.js + * Allows configuring chart js using the provider + * + * angular.module('myModule', ['chart.js']).config(function(ChartJsProvider) { + * ChartJsProvider.setOptions({ responsive: true }); + * ChartJsProvider.setOptions('Line', { responsive: false }); + * }))) + */ + function ChartJsProvider () { + var options = {}; + var ChartJs = { + Chart: Chart, + getOptions: function (type) { + var typeOptions = type && options[type] || {}; + return angular.extend({}, options, typeOptions); + } + }; + + /** + * Allow to set global options during configuration + */ + this.setOptions = function (type, customOptions) { + // If no type was specified set option for the global object + if (! customOptions) { + customOptions = type; + options = angular.extend(options, customOptions); + return; + } + // Set options for the specific chart + options[type] = angular.extend(options[type] || {}, customOptions); + }; + + this.$get = function () { + return ChartJs; + }; + } + + function ChartJsFactory (ChartJs, $timeout) { + return function chart (type) { + return { + restrict: 'CA', + scope: { + data: '=?', + labels: '=?', + options: '=?', + series: '=?', + colours: '=?', + getColour: '=?', + chartType: '=', + legend: '@', + click: '=?', + hover: '=?', + + chartData: '=?', + chartLabels: '=?', + chartOptions: '=?', + chartSeries: '=?', + chartColours: '=?', + chartLegend: '@', + chartClick: '=?', + chartHover: '=?' + }, + link: function (scope, elem/*, attrs */) { + var chart, container = document.createElement('div'); + container.className = 'chart-container'; + elem.replaceWith(container); + container.appendChild(elem[0]); + + if (usingExcanvas) window.G_vmlCanvasManager.initElement(elem[0]); + + ['data', 'labels', 'options', 'series', 'colours', 'legend', 'click', 'hover'].forEach(deprecated); + function aliasVar (fromName, toName) { + scope.$watch(fromName, function (newVal) { + if (typeof newVal === 'undefined') return; + scope[toName] = newVal; + }); + } + /* provide backward compatibility to "old" directive names, by + * having an alias point from the new names to the old names. */ + aliasVar('chartData', 'data'); + aliasVar('chartLabels', 'labels'); + aliasVar('chartOptions', 'options'); + aliasVar('chartSeries', 'series'); + aliasVar('chartColours', 'colours'); + aliasVar('chartLegend', 'legend'); + aliasVar('chartClick', 'click'); + aliasVar('chartHover', 'hover'); + + // Order of setting "watch" matter + + scope.$watch('data', function (newVal, oldVal) { + if (! newVal || ! newVal.length || (Array.isArray(newVal[0]) && ! newVal[0].length)) { + destroyChart(chart, scope); + return; + } + var chartType = type || scope.chartType; + if (! chartType) return; + + if (chart && canUpdateChart(newVal, oldVal)) + return updateChart(chart, newVal, scope, elem); + + createChart(chartType); + }, true); + + scope.$watch('series', resetChart, true); + scope.$watch('labels', resetChart, true); + scope.$watch('options', resetChart, true); + scope.$watch('colours', resetChart, true); + + scope.$watch('chartType', function (newVal, oldVal) { + if (isEmpty(newVal)) return; + if (angular.equals(newVal, oldVal)) return; + createChart(newVal); + }); + + scope.$on('$destroy', function () { + destroyChart(chart, scope); + }); + + function resetChart (newVal, oldVal) { + if (isEmpty(newVal)) return; + if (angular.equals(newVal, oldVal)) return; + var chartType = type || scope.chartType; + if (! chartType) return; + + // chart.update() doesn't work for series and labels + // so we have to re-create the chart entirely + createChart(chartType); + } + + function createChart (type) { + if (isResponsive(type, scope) && elem[0].clientHeight === 0 && container.clientHeight === 0) { + return $timeout(function () { + createChart(type); + }, 50, false); + } + if (! scope.data || ! scope.data.length) return; + scope.getColour = typeof scope.getColour === 'function' ? scope.getColour : getRandomColour; + var colours = getColours(type, scope); + var cvs = elem[0], ctx = cvs.getContext('2d'); + var data = Array.isArray(scope.data[0]) ? + getDataSets(scope.labels, scope.data, scope.series || [], colours) : + getData(scope.labels, scope.data, colours); + var options = angular.extend({}, ChartJs.getOptions(type), scope.options); + + // Destroy old chart if it exists to avoid ghost charts issue + // https://github.com/jtblin/angular-chart.js/issues/187 + destroyChart(chart, scope); + chart = new ChartJs.Chart(ctx)[type](data, options); + scope.$emit('create', chart); + + // Bind events + cvs.onclick = scope.click ? getEventHandler(scope, chart, 'click', false) : angular.noop; + cvs.onmousemove = scope.hover ? getEventHandler(scope, chart, 'hover', true) : angular.noop; + + if (scope.legend && scope.legend !== 'false') setLegend(elem, chart); + } + + function deprecated (attr) { + if (typeof console !== 'undefined' && ChartJs.getOptions().env !== 'test') { + var warn = typeof console.warn === 'function' ? console.warn : console.log; + if (!! scope[attr]) { + warn.call(console, '"%s" is deprecated and will be removed in a future version. ' + + 'Please use "chart-%s" instead.', attr, attr); + } + } + } + } + }; + }; + + function canUpdateChart (newVal, oldVal) { + if (newVal && oldVal && newVal.length && oldVal.length) { + return Array.isArray(newVal[0]) ? + newVal.length === oldVal.length && newVal.every(function (element, index) { + return element.length === oldVal[index].length; }) : + oldVal.reduce(sum, 0) > 0 ? newVal.length === oldVal.length : false; + } + return false; + } + + function sum (carry, val) { + return carry + val; + } + + function getEventHandler (scope, chart, action, triggerOnlyOnChange) { + var lastState = null; + return function (evt) { + var atEvent = chart.getPointsAtEvent || chart.getBarsAtEvent || chart.getSegmentsAtEvent; + if (atEvent) { + var activePoints = atEvent.call(chart, evt); + if (triggerOnlyOnChange === false || angular.equals(lastState, activePoints) === false) { + lastState = activePoints; + scope[action](activePoints, evt); + scope.$apply(); + } + } + }; + } + + function getColours (type, scope) { + var notEnoughColours = false; + var colours = angular.copy(scope.colours || + ChartJs.getOptions(type).colours || + Chart.defaults.global.colours + ); + while (colours.length < scope.data.length) { + colours.push(scope.getColour()); + notEnoughColours = true; + } + // mutate colours in this case as we don't want + // the colours to change on each refresh + if (notEnoughColours) scope.colours = colours; + return colours.map(convertColour); + } + + function convertColour (colour) { + if (typeof colour === 'object' && colour !== null) return colour; + if (typeof colour === 'string' && colour[0] === '#') return getColour(hexToRgb(colour.substr(1))); + return getRandomColour(); + } + + function getRandomColour () { + var colour = [getRandomInt(0, 255), getRandomInt(0, 255), getRandomInt(0, 255)]; + return getColour(colour); + } + + function getColour (colour) { + return { + fillColor: rgba(colour, 0.2), + strokeColor: rgba(colour, 1), + pointColor: rgba(colour, 1), + pointStrokeColor: '#fff', + pointHighlightFill: '#fff', + pointHighlightStroke: rgba(colour, 0.8) + }; + } + + function getRandomInt (min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; + } + + function rgba (colour, alpha) { + if (usingExcanvas) { + // rgba not supported by IE8 + return 'rgb(' + colour.join(',') + ')'; + } else { + return 'rgba(' + colour.concat(alpha).join(',') + ')'; + } + } + + // Credit: http://stackoverflow.com/a/11508164/1190235 + function hexToRgb (hex) { + var bigint = parseInt(hex, 16), + r = (bigint >> 16) & 255, + g = (bigint >> 8) & 255, + b = bigint & 255; + + return [r, g, b]; + } + + function getDataSets (labels, data, series, colours) { + return { + labels: labels, + datasets: data.map(function (item, i) { + return angular.extend({}, colours[i], { + label: series[i], + data: item + }); + }) + }; + } + + function getData (labels, data, colours) { + return labels.map(function (label, i) { + return angular.extend({}, colours[i], { + label: label, + value: data[i], + color: colours[i].strokeColor, + highlight: colours[i].pointHighlightStroke + }); + }); + } + + function setLegend (elem, chart) { + var $parent = elem.parent(), + $oldLegend = $parent.find('chart-legend'), + legend = '' + chart.generateLegend() + ''; + if ($oldLegend.length) $oldLegend.replaceWith(legend); + else $parent.append(legend); + } + + function updateChart (chart, values, scope, elem) { + if (Array.isArray(scope.data[0])) { + chart.datasets.forEach(function (dataset, i) { + (dataset.points || dataset.bars).forEach(function (dataItem, j) { + dataItem.value = values[i][j]; + }); + }); + } else { + chart.segments.forEach(function (segment, i) { + segment.value = values[i]; + }); + } + chart.update(); + scope.$emit('update', chart); + if (scope.legend && scope.legend !== 'false') setLegend(elem, chart); + } + + function isEmpty (value) { + return ! value || + (Array.isArray(value) && ! value.length) || + (typeof value === 'object' && ! Object.keys(value).length); + } + + function isResponsive (type, scope) { + var options = angular.extend({}, Chart.defaults.global, ChartJs.getOptions(type), scope.options); + return options.responsive; + } + + function destroyChart(chart, scope) { + if(! chart) return; + chart.destroy(); + scope.$emit('destroy', chart); + } + } +})); + + + +(function(root, factory) { + if (typeof define === 'function' && define.amd) { + define(["angular","chartist"], factory); + } else if (typeof exports === 'object') { + module.exports = factory(require('angular'), require('chartist')); + } else { + root.angularChartist = factory(root.angular, root.Chartist); + } +}(this, function(angular, Chartist) { + +'use strict'; + +var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +/*global angular, Chartist*/ + +var AngularChartistCtrl = (function () { + function AngularChartistCtrl($scope, $element) { + var _this = this; + + _classCallCheck(this, AngularChartistCtrl); + + this.data = $scope.data; + this.chartType = $scope.chartType; + + this.events = $scope.events() || {}; + this.options = $scope.chartOptions() || null; + this.responsiveOptions = $scope.responsiveOptions() || null; + + this.element = $element[0]; + + this.renderChart(); + + $scope.$watch(function () { + return { + data: $scope.data, + chartType: $scope.chartType, + chartOptions: $scope.chartOptions() + }; + }, this.update.bind(this), true); + + $scope.$on('$destroy', function () { + if (_this.chart) { + _this.chart.detach(); + } + }); + } + + _createClass(AngularChartistCtrl, [{ + key: 'bindEvents', + value: function bindEvents() { + var _this2 = this; + + Object.keys(this.events).forEach(function (eventName) { + _this2.chart.on(eventName, _this2.events[eventName]); + }); + } + }, { + key: 'renderChart', + value: function renderChart() { + // ensure that the chart does not get created without data + if (this.data) { + this.chart = Chartist[this.chartType](this.element, this.data, this.options, this.responsiveOptions); + + this.bindEvents(); + + return this.chart; + } + } + }, { + key: 'update', + value: function update(newConfig, oldConfig) { + // Update controller with new configuration + this.chartType = newConfig.chartType; + this.data = newConfig.data; + this.options = newConfig.chartOptions; + + // If chart type changed we need to recreate whole chart, otherwise we can update + if (!this.chart || newConfig.chartType !== oldConfig.chartType) { + this.renderChart(); + } else { + this.chart.update(this.data, this.options); + } + } + }]); + + return AngularChartistCtrl; +})(); + +AngularChartistCtrl.$inject = ['$scope', '$element']; + +function chartistDirective() { + return { + restrict: 'EA', + scope: { + // mandatory + data: '=chartistData', + chartType: '@chartistChartType', + // optional + events: '&chartistEvents', + chartOptions: '&chartistChartOptions', + responsiveOptions: '&chartistResponsiveOptions' + }, + controller: 'AngularChartistCtrl' + }; +} + +chartistDirective.$inject = []; + +/*eslint-disable no-unused-vars */ +var angularChartist = angular.module('angular-chartist', []).controller('AngularChartistCtrl', AngularChartistCtrl).directive('chartist', chartistDirective); +/*eslint-enable no-unused-vars */ +return angularChartist; + +})); + +/** + * @license AngularJS v1.5.8 + * (c) 2010-2016 Google, Inc. http://angularjs.org + * License: MIT + */ +(function(window, angular) {'use strict'; + +/** + * @ngdoc module + * @name ngCookies + * @description + * + * # ngCookies + * + * The `ngCookies` module provides a convenient wrapper for reading and writing browser cookies. + * + * + *
+ * + * See {@link ngCookies.$cookies `$cookies`} for usage. + */ + + +angular.module('ngCookies', ['ng']). + /** + * @ngdoc provider + * @name $cookiesProvider + * @description + * Use `$cookiesProvider` to change the default behavior of the {@link ngCookies.$cookies $cookies} service. + * */ + provider('$cookies', [function $CookiesProvider() { + /** + * @ngdoc property + * @name $cookiesProvider#defaults + * @description + * + * Object containing default options to pass when setting cookies. + * + * The object may have following properties: + * + * - **path** - `{string}` - The cookie will be available only for this path and its + * sub-paths. By default, this is the URL that appears in your `` tag. + * - **domain** - `{string}` - The cookie will be available only for this domain and + * its sub-domains. For security reasons the user agent will not accept the cookie + * if the current domain is not a sub-domain of this domain or equal to it. + * - **expires** - `{string|Date}` - String of the form "Wdy, DD Mon YYYY HH:MM:SS GMT" + * or a Date object indicating the exact date/time this cookie will expire. + * - **secure** - `{boolean}` - If `true`, then the cookie will only be available through a + * secured connection. + * + * Note: By default, the address that appears in your `` tag will be used as the path. + * This is important so that cookies will be visible for all routes when html5mode is enabled. + * + **/ + var defaults = this.defaults = {}; + + function calcOptions(options) { + return options ? angular.extend({}, defaults, options) : defaults; + } + + /** + * @ngdoc service + * @name $cookies + * + * @description + * Provides read/write access to browser's cookies. + * + *
+ * Up until Angular 1.3, `$cookies` exposed properties that represented the + * current browser cookie values. In version 1.4, this behavior has changed, and + * `$cookies` now provides a standard api of getters, setters etc. + *
+ * + * Requires the {@link ngCookies `ngCookies`} module to be installed. + * + * @example + * + * ```js + * angular.module('cookiesExample', ['ngCookies']) + * .controller('ExampleController', ['$cookies', function($cookies) { + * // Retrieving a cookie + * var favoriteCookie = $cookies.get('myFavorite'); + * // Setting a cookie + * $cookies.put('myFavorite', 'oatmeal'); + * }]); + * ``` + */ + this.$get = ['$$cookieReader', '$$cookieWriter', function($$cookieReader, $$cookieWriter) { + return { + /** + * @ngdoc method + * @name $cookies#get + * + * @description + * Returns the value of given cookie key + * + * @param {string} key Id to use for lookup. + * @returns {string} Raw cookie value. + */ + get: function(key) { + return $$cookieReader()[key]; + }, + + /** + * @ngdoc method + * @name $cookies#getObject + * + * @description + * Returns the deserialized value of given cookie key + * + * @param {string} key Id to use for lookup. + * @returns {Object} Deserialized cookie value. + */ + getObject: function(key) { + var value = this.get(key); + return value ? angular.fromJson(value) : value; + }, + + /** + * @ngdoc method + * @name $cookies#getAll + * + * @description + * Returns a key value object with all the cookies + * + * @returns {Object} All cookies + */ + getAll: function() { + return $$cookieReader(); + }, + + /** + * @ngdoc method + * @name $cookies#put + * + * @description + * Sets a value for given cookie key + * + * @param {string} key Id for the `value`. + * @param {string} value Raw value to be stored. + * @param {Object=} options Options object. + * See {@link ngCookies.$cookiesProvider#defaults $cookiesProvider.defaults} + */ + put: function(key, value, options) { + $$cookieWriter(key, value, calcOptions(options)); + }, + + /** + * @ngdoc method + * @name $cookies#putObject + * + * @description + * Serializes and sets a value for given cookie key + * + * @param {string} key Id for the `value`. + * @param {Object} value Value to be stored. + * @param {Object=} options Options object. + * See {@link ngCookies.$cookiesProvider#defaults $cookiesProvider.defaults} + */ + putObject: function(key, value, options) { + this.put(key, angular.toJson(value), options); + }, + + /** + * @ngdoc method + * @name $cookies#remove + * + * @description + * Remove given cookie + * + * @param {string} key Id of the key-value pair to delete. + * @param {Object=} options Options object. + * See {@link ngCookies.$cookiesProvider#defaults $cookiesProvider.defaults} + */ + remove: function(key, options) { + $$cookieWriter(key, undefined, calcOptions(options)); + } + }; + }]; + }]); + +angular.module('ngCookies'). +/** + * @ngdoc service + * @name $cookieStore + * @deprecated + * @requires $cookies + * + * @description + * Provides a key-value (string-object) storage, that is backed by session cookies. + * Objects put or retrieved from this storage are automatically serialized or + * deserialized by angular's toJson/fromJson. + * + * Requires the {@link ngCookies `ngCookies`} module to be installed. + * + *
+ * **Note:** The $cookieStore service is **deprecated**. + * Please use the {@link ngCookies.$cookies `$cookies`} service instead. + *
+ * + * @example + * + * ```js + * angular.module('cookieStoreExample', ['ngCookies']) + * .controller('ExampleController', ['$cookieStore', function($cookieStore) { + * // Put cookie + * $cookieStore.put('myFavorite','oatmeal'); + * // Get cookie + * var favoriteCookie = $cookieStore.get('myFavorite'); + * // Removing a cookie + * $cookieStore.remove('myFavorite'); + * }]); + * ``` + */ + factory('$cookieStore', ['$cookies', function($cookies) { + + return { + /** + * @ngdoc method + * @name $cookieStore#get + * + * @description + * Returns the value of given cookie key + * + * @param {string} key Id to use for lookup. + * @returns {Object} Deserialized cookie value, undefined if the cookie does not exist. + */ + get: function(key) { + return $cookies.getObject(key); + }, + + /** + * @ngdoc method + * @name $cookieStore#put + * + * @description + * Sets a value for given cookie key + * + * @param {string} key Id for the `value`. + * @param {Object} value Value to be stored. + */ + put: function(key, value) { + $cookies.putObject(key, value); + }, + + /** + * @ngdoc method + * @name $cookieStore#remove + * + * @description + * Remove given cookie + * + * @param {string} key Id of the key-value pair to delete. + */ + remove: function(key) { + $cookies.remove(key); + } + }; + + }]); + +/** + * @name $$cookieWriter + * @requires $document + * + * @description + * This is a private service for writing cookies + * + * @param {string} name Cookie name + * @param {string=} value Cookie value (if undefined, cookie will be deleted) + * @param {Object=} options Object with options that need to be stored for the cookie. + */ +function $$CookieWriter($document, $log, $browser) { + var cookiePath = $browser.baseHref(); + var rawDocument = $document[0]; + + function buildCookieString(name, value, options) { + var path, expires; + options = options || {}; + expires = options.expires; + path = angular.isDefined(options.path) ? options.path : cookiePath; + if (angular.isUndefined(value)) { + expires = 'Thu, 01 Jan 1970 00:00:00 GMT'; + value = ''; + } + if (angular.isString(expires)) { + expires = new Date(expires); + } + + var str = encodeURIComponent(name) + '=' + encodeURIComponent(value); + str += path ? ';path=' + path : ''; + str += options.domain ? ';domain=' + options.domain : ''; + str += expires ? ';expires=' + expires.toUTCString() : ''; + str += options.secure ? ';secure' : ''; + + // per http://www.ietf.org/rfc/rfc2109.txt browser must allow at minimum: + // - 300 cookies + // - 20 cookies per unique domain + // - 4096 bytes per cookie + var cookieLength = str.length + 1; + if (cookieLength > 4096) { + $log.warn("Cookie '" + name + + "' possibly not set or overflowed because it was too large (" + + cookieLength + " > 4096 bytes)!"); + } + + return str; + } + + return function(name, value, options) { + rawDocument.cookie = buildCookieString(name, value, options); + }; +} + +$$CookieWriter.$inject = ['$document', '$log', '$browser']; + +angular.module('ngCookies').provider('$$cookieWriter', function $$CookieWriterProvider() { + this.$get = $$CookieWriter; +}); + + +})(window, window.angular); + +/*! + * angular-datatables - v0.5.4 + * https://github.com/l-lin/angular-datatables + * License: MIT + */ +if (typeof module !== "undefined" && typeof exports !== "undefined" && module.exports === exports) { + module.exports = 'datatables.bootstrap'; +} +(function(window, document, $, angular) { + + 'use strict'; + angular.module('datatables.bootstrap.colvis', ['datatables.bootstrap.options', 'datatables.util']) + .service('DTBootstrapColVis', dtBootstrapColVis); + + /* @ngInject */ + function dtBootstrapColVis(DTPropertyUtil, DTBootstrapDefaultOptions) { + var _initializedColVis = false; + return { + integrate: integrate, + deIntegrate: deIntegrate + }; + + function integrate(addDrawCallbackFunction, bootstrapOptions) { + if (!_initializedColVis) { + var colVisProperties = DTPropertyUtil.overrideProperties( + DTBootstrapDefaultOptions.getOptions().ColVis, + bootstrapOptions ? bootstrapOptions.ColVis : null + ); + /* ColVis Bootstrap compatibility */ + if ($.fn.DataTable.ColVis) { + addDrawCallbackFunction(function() { + $('.ColVis_MasterButton').attr('class', 'ColVis_MasterButton ' + colVisProperties.classes.masterButton); + $('.ColVis_Button').removeClass('ColVis_Button'); + }); + } + + _initializedColVis = true; + } + } + + function deIntegrate() { + if (_initializedColVis && $.fn.DataTable.ColVis) { + _initializedColVis = false; + } + } + } + dtBootstrapColVis.$inject = ['DTPropertyUtil', 'DTBootstrapDefaultOptions']; + + 'use strict'; + + // See http://getbootstrap.com + angular.module('datatables.bootstrap', [ + 'datatables.bootstrap.options', + 'datatables.bootstrap.tabletools', + 'datatables.bootstrap.colvis' + ]) + .config(dtBootstrapConfig) + .run(initBootstrapPlugin) + .service('DTBootstrap', dtBootstrap); + + /* @ngInject */ + function dtBootstrapConfig($provide) { + $provide.decorator('DTOptionsBuilder', dtOptionsBuilderDecorator); + + function dtOptionsBuilderDecorator($delegate) { + var newOptions = $delegate.newOptions; + var fromSource = $delegate.fromSource; + var fromFnPromise = $delegate.fromFnPromise; + + $delegate.newOptions = function() { + return _decorateOptions(newOptions); + }; + $delegate.fromSource = function(ajax) { + return _decorateOptions(fromSource, ajax); + }; + $delegate.fromFnPromise = function(fnPromise) { + return _decorateOptions(fromFnPromise, fnPromise); + }; + + return $delegate; + + function _decorateOptions(fn, params) { + var options = fn(params); + options.withBootstrap = withBootstrap; + options.withBootstrapOptions = withBootstrapOptions; + return options; + + /** + * Add bootstrap compatibility + * @returns {DTOptions} the options + */ + function withBootstrap() { + options.hasBootstrap = true; + // Override page button active CSS class + if (angular.isObject(options.oClasses)) { + options.oClasses.sPageButtonActive = 'active'; + } else { + options.oClasses = { + sPageButtonActive: 'active' + }; + } + return options; + } + + /** + * Add bootstrap options + * @param bootstrapOptions the bootstrap options + * @returns {DTOptions} the options + */ + function withBootstrapOptions(bootstrapOptions) { + options.bootstrap = bootstrapOptions; + return options; + } + } + } + dtOptionsBuilderDecorator.$inject = ['$delegate']; + } + dtBootstrapConfig.$inject = ['$provide']; + + /* @ngInject */ + function initBootstrapPlugin(DTRendererService, DTBootstrap) { + var columnFilterPlugin = { + preRender: preRender + }; + DTRendererService.registerPlugin(columnFilterPlugin); + + function preRender(options) { + // Integrate bootstrap (or not) + if (options && options.hasBootstrap) { + DTBootstrap.integrate(options); + } else { + DTBootstrap.deIntegrate(); + } + } + } + initBootstrapPlugin.$inject = ['DTRendererService', 'DTBootstrap']; + + /** + * Source: https://editor.datatables.net/release/DataTables/extras/Editor/examples/bootstrap.html + */ + /* @ngInject */ + function dtBootstrap(DTBootstrapTableTools, DTBootstrapColVis, DTBootstrapDefaultOptions, DTPropertyUtil) { + var _initialized = false, + _drawCallbackFunctionList = [], + _savedFn = {}; + + return { + integrate: integrate, + deIntegrate: deIntegrate + }; + + function _saveFnToBeOverrided() { + _savedFn.oStdClasses = angular.copy($.fn.dataTableExt.oStdClasses); + _savedFn.fnPagingInfo = $.fn.dataTableExt.oApi.fnPagingInfo; + _savedFn.renderer = angular.copy($.fn.DataTable.ext.renderer); + if ($.fn.DataTable.TableTools) { + _savedFn.TableTools = { + classes: angular.copy($.fn.DataTable.TableTools.classes), + oTags: angular.copy($.fn.DataTable.TableTools.DEFAULTS.oTags) + }; + } + } + + function _revertToDTFn() { + $.extend($.fn.dataTableExt.oStdClasses, _savedFn.oStdClasses); + $.fn.dataTableExt.oApi.fnPagingInfo = _savedFn.fnPagingInfo; + $.extend(true, $.fn.DataTable.ext.renderer, _savedFn.renderer); + } + + function _overrideClasses() { + /* Default class modification */ + $.extend($.fn.dataTableExt.oStdClasses, { + 'sWrapper': 'dataTables_wrapper form-inline', + 'sFilterInput': 'form-control input-sm', + 'sLengthSelect': 'form-control input-sm', + 'sFilter': 'dataTables_filter', + 'sLength': 'dataTables_length' + }); + } + + function _overridePagingInfo() { + /* API method to get paging information */ + $.fn.dataTableExt.oApi.fnPagingInfo = function(oSettings) { + return { + 'iStart': oSettings._iDisplayStart, + 'iEnd': oSettings.fnDisplayEnd(), + 'iLength': oSettings._iDisplayLength, + 'iTotal': oSettings.fnRecordsTotal(), + 'iFilteredTotal': oSettings.fnRecordsDisplay(), + 'iPage': oSettings._iDisplayLength === -1 ? 0 : Math.ceil(oSettings._iDisplayStart / oSettings._iDisplayLength), + 'iTotalPages': oSettings._iDisplayLength === -1 ? 0 : Math.ceil(oSettings.fnRecordsDisplay() / oSettings._iDisplayLength) + }; + }; + } + + function _overridePagination(bootstrapOptions) { + // Note: Copy paste with some changes from DataTables v1.10.1 source code + $.extend(true, $.fn.DataTable.ext.renderer, { + pageButton: { + _: function(settings, host, idx, buttons, page, pages) { + var classes = settings.oClasses; + var lang = settings.language ? settings.language.oPaginate : settings.oLanguage.oPaginate; + var btnDisplay, btnClass, counter = 0; + var paginationClasses = DTPropertyUtil.overrideProperties( + DTBootstrapDefaultOptions.getOptions().pagination, + bootstrapOptions ? bootstrapOptions.pagination : null + ); + var $paginationContainer = $('
    ', { + 'class': paginationClasses.classes.ul + }); + + var attach = function(container, buttons) { + var i, ien, node, button; + var clickHandler = function(e) { + e.preventDefault(); + // IMPORTANT: Reference to internal functions of DT. It might change between versions + $.fn.DataTable.ext.internal._fnPageChange(settings, e.data.action, true); + }; + + + for (i = 0, ien = buttons.length; i < ien; i++) { + button = buttons[i]; + + if ($.isArray(button)) { + // Override DT element + button.DT_el = 'li'; + var inner = $('<' + (button.DT_el || 'div') + '/>') + .appendTo($paginationContainer); + attach(inner, button); + } else { + btnDisplay = ''; + btnClass = ''; + var $paginationBtn = $('
  • '), + isDisabled; + + switch (button) { + case 'ellipsis': + $paginationContainer.append('
  • '); + break; + + case 'first': + btnDisplay = lang.sFirst; + btnClass = button; + if (page <= 0) { + $paginationBtn.addClass(classes.sPageButtonDisabled); + isDisabled = true; + } + break; + + case 'previous': + btnDisplay = lang.sPrevious; + btnClass = button; + if (page <= 0) { + $paginationBtn.addClass(classes.sPageButtonDisabled); + isDisabled = true; + } + break; + + case 'next': + btnDisplay = lang.sNext; + btnClass = button; + if (page >= pages - 1) { + $paginationBtn.addClass(classes.sPageButtonDisabled); + isDisabled = true; + } + break; + + case 'last': + btnDisplay = lang.sLast; + btnClass = button; + if (page >= pages - 1) { + $paginationBtn.addClass(classes.sPageButtonDisabled); + isDisabled = true; + } + break; + + default: + btnDisplay = button + 1; + btnClass = ''; + if (page === button) { + $paginationBtn.addClass(classes.sPageButtonActive); + } + break; + } + + if (btnDisplay) { + $paginationBtn.appendTo($paginationContainer); + node = $('', { + 'href': '#', + 'class': btnClass, + 'aria-controls': settings.sTableId, + 'data-dt-idx': counter, + 'tabindex': settings.iTabIndex, + 'id': idx === 0 && typeof button === 'string' ? + settings.sTableId + '_' + button : null + }) + .html(btnDisplay) + .appendTo($paginationBtn); + + // IMPORTANT: Reference to internal functions of DT. It might change between versions + $.fn.DataTable.ext.internal._fnBindAction( + node, { + action: button + }, clickHandler + ); + + counter++; + } + } + } + }; + + // IE9 throws an 'unknown error' if document.activeElement is used + // inside an iframe or frame. Try / catch the error. Not good for + // accessibility, but neither are frames. + try { + // Because this approach is destroying and recreating the paging + // elements, focus is lost on the select button which is bad for + // accessibility. So we want to restore focus once the draw has + // completed + var activeEl = $(document.activeElement).data('dt-idx'); + + // Add
      to the pagination + var container = $(host).empty(); + $paginationContainer.appendTo(container); + attach(container, buttons); + + if (activeEl !== null) { + $(host).find('[data-dt-idx=' + activeEl + ']').focus(); + } + } catch (e) {} + } + } + }); + } + + function _addDrawCallbackFunction(fn) { + if (angular.isFunction(fn)) { + _drawCallbackFunctionList.push(fn); + } + } + + function _init(bootstrapOptions) { + if (!_initialized) { + _saveFnToBeOverrided(); + _overrideClasses(); + _overridePagingInfo(); + _overridePagination(bootstrapOptions); + + _addDrawCallbackFunction(function() { + $('div.dataTables_filter').find('input').addClass('form-control'); + $('div.dataTables_length').find('select').addClass('form-control'); + }); + + _initialized = true; + } + } + + function _setDom(options) { + if (!options.dom || options.dom === $.fn.dataTable.defaults.sDom) { + return DTBootstrapDefaultOptions.getOptions().dom; + } + return options.dom; + } + /** + * Integrate Bootstrap + * @param options the datatables options + */ + function integrate(options) { + _init(options.bootstrap); + DTBootstrapTableTools.integrate(options.bootstrap); + DTBootstrapColVis.integrate(_addDrawCallbackFunction, options.bootstrap); + + options.dom = _setDom(options); + if (angular.isUndefined(options.fnDrawCallback)) { + // Call every drawcallback functions + options.fnDrawCallback = function() { + for (var index = 0; index < _drawCallbackFunctionList.length; index++) { + _drawCallbackFunctionList[index](); + } + }; + } + } + + function deIntegrate() { + if (_initialized) { + _revertToDTFn(); + DTBootstrapTableTools.deIntegrate(); + DTBootstrapColVis.deIntegrate(); + _initialized = false; + } + } + } + dtBootstrap.$inject = ['DTBootstrapTableTools', 'DTBootstrapColVis', 'DTBootstrapDefaultOptions', 'DTPropertyUtil']; + + 'use strict'; + angular.module('datatables.bootstrap.options', ['datatables.options', 'datatables.util']) + .constant('DT_BOOTSTRAP_DEFAULT_OPTIONS', { + TableTools: { + classes: { + container: 'DTTT btn-group', + buttons: { + normal: 'btn btn-default', + disabled: 'disabled' + }, + collection: { + container: 'DTTT_dropdown dropdown-menu', + buttons: { + normal: '', + disabled: 'disabled' + } + }, + print: { + info: 'DTTT_print_info modal' + }, + select: { + row: 'active' + } + }, + DEFAULTS: { + oTags: { + collection: { + container: 'ul', + button: 'li', + liner: 'a' + } + } + } + }, + ColVis: { + classes: { + masterButton: 'btn btn-default' + } + }, + pagination: { + classes: { + ul: 'pagination' + } + }, + dom: '<\'row\'<\'col-xs-6\'l><\'col-xs-6\'f>r>t<\'row\'<\'col-xs-6\'i><\'col-xs-6\'p>>' + }) + .factory('DTBootstrapDefaultOptions', dtBootstrapDefaultOptions); + + /* @ngInject */ + function dtBootstrapDefaultOptions(DTDefaultOptions, DTPropertyUtil, DT_BOOTSTRAP_DEFAULT_OPTIONS) { + return { + getOptions: getOptions + }; + /** + * Get the default options for bootstrap integration + * @returns {*} the bootstrap default options + */ + function getOptions() { + return DTPropertyUtil.overrideProperties(DT_BOOTSTRAP_DEFAULT_OPTIONS, DTDefaultOptions.bootstrapOptions); + } + } + dtBootstrapDefaultOptions.$inject = ['DTDefaultOptions', 'DTPropertyUtil', 'DT_BOOTSTRAP_DEFAULT_OPTIONS']; + + 'use strict'; + + angular.module('datatables.bootstrap.tabletools', ['datatables.bootstrap.options', 'datatables.util']) + .service('DTBootstrapTableTools', dtBootstrapTableTools); + + /* @ngInject */ + function dtBootstrapTableTools(DTPropertyUtil, DTBootstrapDefaultOptions) { + var _initializedTableTools = false, + _savedFn = {}; + + return { + integrate: integrate, + deIntegrate: deIntegrate + }; + + function integrate(bootstrapOptions) { + if (!_initializedTableTools) { + _saveFnToBeOverrided(); + + /* + * TableTools Bootstrap compatibility + * Required TableTools 2.1+ + */ + if ($.fn.DataTable.TableTools) { + var tableToolsOptions = DTPropertyUtil.overrideProperties( + DTBootstrapDefaultOptions.getOptions().TableTools, + bootstrapOptions ? bootstrapOptions.TableTools : null + ); + // Set the classes that TableTools uses to something suitable for Bootstrap + $.extend(true, $.fn.DataTable.TableTools.classes, tableToolsOptions.classes); + + // Have the collection use a bootstrap compatible dropdown + $.extend(true, $.fn.DataTable.TableTools.DEFAULTS.oTags, tableToolsOptions.DEFAULTS.oTags); + } + + _initializedTableTools = true; + } + } + + function deIntegrate() { + if (_initializedTableTools && $.fn.DataTable.TableTools && _savedFn.TableTools) { + $.extend(true, $.fn.DataTable.TableTools.classes, _savedFn.TableTools.classes); + $.extend(true, $.fn.DataTable.TableTools.DEFAULTS.oTags, _savedFn.TableTools.oTags); + _initializedTableTools = false; + } + } + + function _saveFnToBeOverrided() { + if ($.fn.DataTable.TableTools) { + _savedFn.TableTools = { + classes: angular.copy($.fn.DataTable.TableTools.classes), + oTags: angular.copy($.fn.DataTable.TableTools.DEFAULTS.oTags) + }; + } + } + } + dtBootstrapTableTools.$inject = ['DTPropertyUtil', 'DTBootstrapDefaultOptions']; + + +})(window, document, jQuery, angular); + +/*! + * angular-datatables - v0.5.4 + * https://github.com/l-lin/angular-datatables + * License: MIT + */ +if (typeof module !== "undefined" && typeof exports !== "undefined" && module.exports === exports) { + module.exports = 'datatables.buttons'; +} +(function(window, document, $, angular) { + + 'use strict'; + + // See https://datatables.net/extensions/buttons/ + angular.module('datatables.buttons', ['datatables']) + .config(dtButtonsConfig) + .run(initButtonsPlugin); + + /* @ngInject */ + function dtButtonsConfig($provide) { + $provide.decorator('DTOptionsBuilder', dtOptionsBuilderDecorator); + + function dtOptionsBuilderDecorator($delegate) { + var newOptions = $delegate.newOptions; + var fromSource = $delegate.fromSource; + var fromFnPromise = $delegate.fromFnPromise; + + $delegate.newOptions = function() { + return _decorateOptions(newOptions); + }; + $delegate.fromSource = function(ajax) { + return _decorateOptions(fromSource, ajax); + }; + $delegate.fromFnPromise = function(fnPromise) { + return _decorateOptions(fromFnPromise, fnPromise); + }; + + return $delegate; + + function _decorateOptions(fn, params) { + var options = fn(params); + options.withButtons = withButtons; + return options; + + /** + * Add buttons compatibility + * @param buttonsOptions the options of the buttons extension (see https://datatables.net/reference/option/buttons#Examples) + * @returns {DTOptions} the options + */ + function withButtons(buttonsOptions) { + var buttonsPrefix = 'B'; + options.dom = options.dom ? options.dom : $.fn.dataTable.defaults.sDom; + if (options.dom.indexOf(buttonsPrefix) === -1) { + options.dom = buttonsPrefix + options.dom; + } + if (angular.isUndefined(buttonsOptions)) { + throw new Error('You must define the options for the button extension. See https://datatables.net/reference/option/buttons#Examples for some example'); + } + options.buttons = buttonsOptions; + return options; + } + } + } + dtOptionsBuilderDecorator.$inject = ['$delegate']; + } + dtButtonsConfig.$inject = ['$provide']; + + /* @ngInject */ + function initButtonsPlugin(DTRendererService) { + var buttonsPlugin = { + preRender: preRender, + postRender: postRender + }; + DTRendererService.registerPlugin(buttonsPlugin); + + function preRender(options) { + if (options && angular.isArray(options.buttons)) { + // The extension buttons seems to clear the content of the options.buttons for some reasons... + // So, we save it in a tmp variable so that we can restore it afterwards + // See https://github.com/l-lin/angular-datatables/issues/502 + options.buttonsTmp = options.buttons.slice(); + } + } + + function postRender(options) { + if (options && angular.isDefined(options.buttonsTmp)) { + // Restore the buttons options + options.buttons = options.buttonsTmp; + delete options.buttonsTmp; + } + } + } + initButtonsPlugin.$inject = ['DTRendererService']; + + +})(window, document, jQuery, angular); + +/*! + * angular-datatables - v0.5.4 + * https://github.com/l-lin/angular-datatables + * License: MIT + */ +if (typeof module !== "undefined" && typeof exports !== "undefined" && module.exports === exports) { + module.exports = 'datatables.colreorder'; +} +(function(window, document, $, angular) { + + 'use strict'; + + // See https://datatables.net/extras/colreorder/ + angular.module('datatables.colreorder', ['datatables']) + .config(dtColReorderConfig); + + /* @ngInject */ + function dtColReorderConfig($provide) { + $provide.decorator('DTOptionsBuilder', dtOptionsBuilderDecorator); + + function dtOptionsBuilderDecorator($delegate) { + var newOptions = $delegate.newOptions; + var fromSource = $delegate.fromSource; + var fromFnPromise = $delegate.fromFnPromise; + + $delegate.newOptions = function() { + return _decorateOptions(newOptions); + }; + $delegate.fromSource = function(ajax) { + return _decorateOptions(fromSource, ajax); + }; + $delegate.fromFnPromise = function(fnPromise) { + return _decorateOptions(fromFnPromise, fnPromise); + }; + + return $delegate; + + function _decorateOptions(fn, params) { + var options = fn(params); + options.withColReorder = withColReorder; + options.withColReorderOption = withColReorderOption; + options.withColReorderOrder = withColReorderOrder; + options.withColReorderCallback = withColReorderCallback; + return options; + + /** + * Add colReorder compatibility + * @returns {DTOptions} the options + */ + function withColReorder() { + var colReorderPrefix = 'R'; + options.dom = options.dom ? options.dom : $.fn.dataTable.defaults.sDom; + if (options.dom.indexOf(colReorderPrefix) === -1) { + options.dom = colReorderPrefix + options.dom; + } + options.hasColReorder = true; + return options; + } + + /** + * Add option to "oColReorder" option + * @param key the key of the option to add + * @param value an object or a function of the function + * @return {DTOptions} the options + */ + function withColReorderOption(key, value) { + if (angular.isString(key)) { + options.oColReorder = options.oColReorder && options.oColReorder !== null ? options.oColReorder : {}; + options.oColReorder[key] = value; + } + return options; + } + + /** + * Set the default column order + * @param aiOrder the column order + * @returns {DTOptions} the options + */ + function withColReorderOrder(aiOrder) { + if (angular.isArray(aiOrder)) { + options.withColReorderOption('aiOrder', aiOrder); + } + return options; + } + + /** + * Set the reorder callback function + * @param fnReorderCallback the callback + * @returns {DTOptions} the options + */ + function withColReorderCallback(fnReorderCallback) { + if (angular.isFunction(fnReorderCallback)) { + options.withColReorderOption('fnReorderCallback', fnReorderCallback); + } else { + throw new Error('The reorder callback must be a function'); + } + return options; + } + } + } + dtOptionsBuilderDecorator.$inject = ['$delegate']; + } + dtColReorderConfig.$inject = ['$provide']; + + +})(window, document, jQuery, angular); + +/*! + * angular-datatables - v0.5.4 + * https://github.com/l-lin/angular-datatables + * License: MIT + */ +if (typeof module !== "undefined" && typeof exports !== "undefined" && module.exports === exports) { + module.exports = 'datatables.columnfilter'; +} +(function(window, document, $, angular) { + + 'use strict'; + + // See http://jquery-datatables-column-filter.googlecode.com/svn/trunk/index.html + angular.module('datatables.columnfilter', ['datatables']) + .config(dtColumnFilterConfig) + .run(initColumnFilterPlugin); + + /* @ngInject */ + function dtColumnFilterConfig($provide) { + $provide.decorator('DTOptionsBuilder', dtOptionsBuilderDecorator); + + function dtOptionsBuilderDecorator($delegate) { + var newOptions = $delegate.newOptions; + var fromSource = $delegate.fromSource; + var fromFnPromise = $delegate.fromFnPromise; + + $delegate.newOptions = function() { + return _decorateOptions(newOptions); + }; + $delegate.fromSource = function(ajax) { + return _decorateOptions(fromSource, ajax); + }; + $delegate.fromFnPromise = function(fnPromise) { + return _decorateOptions(fromFnPromise, fnPromise); + }; + + return $delegate; + + function _decorateOptions(fn, params) { + var options = fn(params); + options.withColumnFilter = withColumnFilter; + return options; + + /** + * Add column filter support + * @param columnFilterOptions the plugins options + * @returns {DTOptions} the options + */ + function withColumnFilter(columnFilterOptions) { + options.hasColumnFilter = true; + if (columnFilterOptions) { + options.columnFilterOptions = columnFilterOptions; + } + return options; + } + } + } + dtOptionsBuilderDecorator.$inject = ['$delegate']; + } + dtColumnFilterConfig.$inject = ['$provide']; + + /* @ngInject */ + function initColumnFilterPlugin(DTRendererService) { + var columnFilterPlugin = { + postRender: postRender + }; + DTRendererService.registerPlugin(columnFilterPlugin); + + function postRender(options, result) { + if (options && options.hasColumnFilter) { + result.dataTable.columnFilter(options.columnFilterOptions); + } + } + } + initColumnFilterPlugin.$inject = ['DTRendererService']; + + +})(window, document, jQuery, angular); + +/*! + * angular-datatables - v0.5.4 + * https://github.com/l-lin/angular-datatables + * License: MIT + */ +if (typeof module !== "undefined" && typeof exports !== "undefined" && module.exports === exports) { + module.exports = 'datatables.colvis'; +} +(function(window, document, $, angular) { + + 'use strict'; + + // See https://datatables.net/extras/colvis/ + angular.module('datatables.colvis', ['datatables']) + .config(dtColVisConfig); + + /* @ngInject */ + function dtColVisConfig($provide) { + $provide.decorator('DTOptionsBuilder', dtOptionsBuilderDecorator); + + function dtOptionsBuilderDecorator($delegate) { + var newOptions = $delegate.newOptions; + var fromSource = $delegate.fromSource; + var fromFnPromise = $delegate.fromFnPromise; + + $delegate.newOptions = function() { + return _decorateOptions(newOptions); + }; + $delegate.fromSource = function(ajax) { + return _decorateOptions(fromSource, ajax); + }; + $delegate.fromFnPromise = function(fnPromise) { + return _decorateOptions(fromFnPromise, fnPromise); + }; + + return $delegate; + + function _decorateOptions(fn, params) { + var options = fn(params); + options.withColVis = withColVis; + options.withColVisOption = withColVisOption; + options.withColVisStateChange = withColVisStateChange; + return options; + + /** + * Add colVis compatibility + * @returns {DTOptions} the options + */ + function withColVis() { + console.warn('The colvis extension has been retired. Please use the button extension instead: https://datatables.net/extensions/buttons/'); + var colVisPrefix = 'C'; + options.dom = options.dom ? options.dom : $.fn.dataTable.defaults.sDom; + if (options.dom.indexOf(colVisPrefix) === -1) { + options.dom = colVisPrefix + options.dom; + } + options.hasColVis = true; + return options; + } + + /** + * Add option to "oColVis" option + * @param key the key of the option to add + * @param value an object or a function of the function + * @returns {DTOptions} the options + */ + function withColVisOption(key, value) { + if (angular.isString(key)) { + options.oColVis = options.oColVis && options.oColVis !== null ? options.oColVis : {}; + options.oColVis[key] = value; + } + return options; + } + + /** + * Set the state change function + * @param fnStateChange the state change function + * @returns {DTOptions} the options + */ + function withColVisStateChange(fnStateChange) { + if (angular.isFunction(fnStateChange)) { + options.withColVisOption('fnStateChange', fnStateChange); + } else { + throw new Error('The state change must be a function'); + } + return options; + } + } + } + dtOptionsBuilderDecorator.$inject = ['$delegate']; + } + dtColVisConfig.$inject = ['$provide']; + + +})(window, document, jQuery, angular); + +/*! + * angular-datatables - v0.5.4 + * https://github.com/l-lin/angular-datatables + * License: MIT + */ +if (typeof module !== "undefined" && typeof exports !== "undefined" && module.exports === exports) { + module.exports = 'datatables.fixedcolumns'; +} +(function(window, document, $, angular) { + + 'use strict'; + + // See https://datatables.net/extensions/fixedcolumns/ + angular.module('datatables.fixedcolumns', ['datatables']) + .config(dtFixedColumnsConfig); + + /* @ngInject */ + function dtFixedColumnsConfig($provide) { + $provide.decorator('DTOptionsBuilder', dtOptionsBuilderDecorator); + + function dtOptionsBuilderDecorator($delegate) { + var newOptions = $delegate.newOptions; + var fromSource = $delegate.fromSource; + var fromFnPromise = $delegate.fromFnPromise; + + $delegate.newOptions = function() { + return _decorateOptions(newOptions); + }; + $delegate.fromSource = function(ajax) { + return _decorateOptions(fromSource, ajax); + }; + $delegate.fromFnPromise = function(fnPromise) { + return _decorateOptions(fromFnPromise, fnPromise); + }; + + return $delegate; + + function _decorateOptions(fn, params) { + var options = fn(params); + options.withFixedColumns = withFixedColumns; + return options; + + /** + * Add fixed columns support + * @param fixedColumnsOptions the plugin options + * @returns {DTOptions} the options + */ + function withFixedColumns(fixedColumnsOptions) { + options.fixedColumns = true; + if (fixedColumnsOptions) { + options.fixedColumns = fixedColumnsOptions; + } + return options; + } + } + } + dtOptionsBuilderDecorator.$inject = ['$delegate']; + } + dtFixedColumnsConfig.$inject = ['$provide']; + + +})(window, document, jQuery, angular); + +/*! + * angular-datatables - v0.5.4 + * https://github.com/l-lin/angular-datatables + * License: MIT + */ +if (typeof module !== "undefined" && typeof exports !== "undefined" && module.exports === exports) { + module.exports = 'datatables.fixedheader'; +} +(function(window, document, $, angular) { + + 'use strict'; + + // See https://datatables.net/extensions/fixedheader/ + angular.module('datatables.fixedheader', ['datatables']) + .config(dtFixedHeaderConfig) + .run(initFixedHeaderPlugin); + + /* @ngInject */ + function dtFixedHeaderConfig($provide) { + $provide.decorator('DTOptionsBuilder', dtOptionsBuilderDecorator); + + function dtOptionsBuilderDecorator($delegate) { + var newOptions = $delegate.newOptions; + var fromSource = $delegate.fromSource; + var fromFnPromise = $delegate.fromFnPromise; + + $delegate.newOptions = function() { + return _decorateOptions(newOptions); + }; + $delegate.fromSource = function(ajax) { + return _decorateOptions(fromSource, ajax); + }; + $delegate.fromFnPromise = function(fnPromise) { + return _decorateOptions(fromFnPromise, fnPromise); + }; + + return $delegate; + + function _decorateOptions(fn, params) { + var options = fn(params); + options.withFixedHeader = withFixedHeader; + return options; + + /** + * Add fixed header support + * @param fixedHeaderOptions the plugin options + * @returns {DTOptions} the options + */ + function withFixedHeader(fixedHeaderOptions) { + options.hasFixedHeader = true; + if (fixedHeaderOptions) { + options.fixedHeaderOptions = fixedHeaderOptions; + } + return options; + } + } + } + dtOptionsBuilderDecorator.$inject = ['$delegate']; + } + dtFixedHeaderConfig.$inject = ['$provide']; + + /* @ngInject */ + function initFixedHeaderPlugin(DTRendererService) { + var fixedHeaderPlugin = { + postRender: postRender + }; + DTRendererService.registerPlugin(fixedHeaderPlugin); + + function postRender(options, result) { + if (options && options.hasFixedHeader) { + new $.fn.dataTable.FixedHeader(result.DataTable, options.fixedHeaderOptions); + } + } + } + initFixedHeaderPlugin.$inject = ['DTRendererService']; + + +})(window, document, jQuery, angular); + +/*! + * angular-datatables - v0.5.4 + * https://github.com/l-lin/angular-datatables + * License: MIT + */ +if (typeof module !== "undefined" && typeof exports !== "undefined" && module.exports === exports) { + module.exports = 'datatables'; +} +(function(window, document, $, angular) { + + 'use strict'; + + angular.module('datatables.directive', ['datatables.instances', 'datatables.renderer', 'datatables.options', 'datatables.util']) + .directive('datatable', dataTable); + + /* @ngInject */ + function dataTable($q, $http, DTRendererFactory, DTRendererService, DTPropertyUtil) { + compileDirective.$inject = ['tElm']; + ControllerDirective.$inject = ['$scope']; + return { + restrict: 'A', + scope: { + dtOptions: '=', + dtColumns: '=', + dtColumnDefs: '=', + datatable: '@', + dtInstance: '=' + }, + compile: compileDirective, + controller: ControllerDirective + }; + + /* @ngInject */ + function compileDirective(tElm) { + var _staticHTML = tElm[0].innerHTML; + + return function postLink($scope, $elem, iAttrs, ctrl) { + function handleChanges(newVal, oldVal) { + if (newVal !== oldVal) { + ctrl.render($elem, ctrl.buildOptionsPromise(), _staticHTML); + } + } + + // Options can hold heavy data, and other deep/large objects. + // watchcollection can improve this by only watching shallowly + var watchFunction = iAttrs.dtDisableDeepWatchers ? '$watchCollection' : '$watch'; + angular.forEach(['dtColumns', 'dtColumnDefs', 'dtOptions'], function(tableDefField) { + $scope[watchFunction].call($scope, tableDefField, handleChanges, true); + }); + DTRendererService.showLoading($elem, $scope); + ctrl.render($elem, ctrl.buildOptionsPromise(), _staticHTML); + }; + } + + /* @ngInject */ + function ControllerDirective($scope) { + var _dtInstance; + var vm = this; + vm.buildOptionsPromise = buildOptionsPromise; + vm.render = render; + + function buildOptionsPromise() { + var defer = $q.defer(); + // Build options + $q.all([ + $q.when($scope.dtOptions), + $q.when($scope.dtColumns), + $q.when($scope.dtColumnDefs) + ]).then(function(results) { + var dtOptions = results[0], + dtColumns = results[1], + dtColumnDefs = results[2]; + // Since Angular 1.3, the promise throws a "Maximum call stack size exceeded" when cloning + // See https://github.com/l-lin/angular-datatables/issues/110 + DTPropertyUtil.deleteProperty(dtOptions, '$promise'); + DTPropertyUtil.deleteProperty(dtColumns, '$promise'); + DTPropertyUtil.deleteProperty(dtColumnDefs, '$promise'); + var options; + if (angular.isDefined(dtOptions)) { + options = {}; + angular.extend(options, dtOptions); + // Set the columns + if (angular.isArray(dtColumns)) { + options.aoColumns = dtColumns; + } + + // Set the column defs + if (angular.isArray(dtColumnDefs)) { + options.aoColumnDefs = dtColumnDefs; + } + + // HACK to resolve the language source manually instead of DT + // See https://github.com/l-lin/angular-datatables/issues/181 + if (options.language && options.language.url) { + var languageDefer = $q.defer(); + $http.get(options.language.url).success(function(language) { + languageDefer.resolve(language); + }); + options.language = languageDefer.promise; + } + + } + return DTPropertyUtil.resolveObjectPromises(options, ['data', 'aaData', 'fnPromise']); + }).then(function(options) { + defer.resolve(options); + }); + return defer.promise; + } + + function render($elem, optionsPromise, staticHTML) { + optionsPromise.then(function(options) { + DTRendererService.preRender(options); + + var isNgDisplay = $scope.datatable && $scope.datatable === 'ng'; + // Render dataTable + if (_dtInstance && _dtInstance._renderer) { + _dtInstance._renderer.withOptions(options) + .render($elem, $scope, staticHTML).then(function(dtInstance) { + _dtInstance = dtInstance; + _setDTInstance(dtInstance); + }); + } else { + DTRendererFactory.fromOptions(options, isNgDisplay) + .render($elem, $scope, staticHTML).then(function(dtInstance) { + _dtInstance = dtInstance; + _setDTInstance(dtInstance); + }); + } + }); + } + + function _setDTInstance(dtInstance) { + if (angular.isFunction($scope.dtInstance)) { + $scope.dtInstance(dtInstance); + } else if (angular.isDefined($scope.dtInstance)) { + $scope.dtInstance = dtInstance; + } + } + } + } + dataTable.$inject = ['$q', '$http', 'DTRendererFactory', 'DTRendererService', 'DTPropertyUtil']; + + 'use strict'; + angular.module('datatables.factory', []) + .factory('DTOptionsBuilder', dtOptionsBuilder) + .factory('DTColumnBuilder', dtColumnBuilder) + .factory('DTColumnDefBuilder', dtColumnDefBuilder) + .factory('DTLoadingTemplate', dtLoadingTemplate); + + /* @ngInject */ + function dtOptionsBuilder() { + /** + * The wrapped datatables options class + * @param sAjaxSource the ajax source to fetch the data + * @param fnPromise the function that returns a promise to fetch the data + */ + var DTOptions = { + /** + * Add the option to the datatables options + * @param key the key of the option + * @param value an object or a function of the option + * @returns {DTOptions} the options + */ + withOption: function(key, value) { + if (angular.isString(key)) { + this[key] = value; + } + return this; + }, + + /** + * Add the Ajax source to the options. + * This corresponds to the "ajax" option + * @param ajax the ajax source + * @returns {DTOptions} the options + */ + withSource: function(ajax) { + this.ajax = ajax; + return this; + }, + + /** + * Add the ajax data properties. + * @param sAjaxDataProp the ajax data property + * @returns {DTOptions} the options + */ + withDataProp: function(sAjaxDataProp) { + this.sAjaxDataProp = sAjaxDataProp; + return this; + }, + + /** + * Set the server data function. + * @param fn the function of the server retrieval + * @returns {DTOptions} the options + */ + withFnServerData: function(fn) { + if (!angular.isFunction(fn)) { + throw new Error('The parameter must be a function'); + } + this.fnServerData = fn; + return this; + }, + + /** + * Set the pagination type. + * @param sPaginationType the pagination type + * @returns {DTOptions} the options + */ + withPaginationType: function(sPaginationType) { + if (angular.isString(sPaginationType)) { + this.sPaginationType = sPaginationType; + } else { + throw new Error('The pagination type must be provided'); + } + return this; + }, + + /** + * Set the language of the datatables + * @param language the language + * @returns {DTOptions} the options + */ + withLanguage: function(language) { + this.language = language; + return this; + }, + + /** + * Set the language source + * @param languageSource the language source + * @returns {DTOptions} the options + */ + withLanguageSource: function(languageSource) { + return this.withLanguage({ + url: languageSource + }); + }, + + /** + * Set default number of items per page to display + * @param iDisplayLength the number of items per page + * @returns {DTOptions} the options + */ + withDisplayLength: function(iDisplayLength) { + this.iDisplayLength = iDisplayLength; + return this; + }, + + /** + * Set the promise to fetch the data + * @param fnPromise the function that returns a promise + * @returns {DTOptions} the options + */ + withFnPromise: function(fnPromise) { + this.fnPromise = fnPromise; + return this; + }, + + /** + * Set the Dom of the DataTables. + * @param dom the dom + * @returns {DTOptions} the options + */ + withDOM: function(dom) { + this.dom = dom; + return this; + } + }; + + return { + /** + * Create a wrapped datatables options + * @returns {DTOptions} a wrapped datatables option + */ + newOptions: function() { + return Object.create(DTOptions); + }, + /** + * Create a wrapped datatables options with the ajax source setted + * @param ajax the ajax source + * @returns {DTOptions} a wrapped datatables option + */ + fromSource: function(ajax) { + var options = Object.create(DTOptions); + options.ajax = ajax; + return options; + }, + /** + * Create a wrapped datatables options with the data promise. + * @param fnPromise the function that returns a promise to fetch the data + * @returns {DTOptions} a wrapped datatables option + */ + fromFnPromise: function(fnPromise) { + var options = Object.create(DTOptions); + options.fnPromise = fnPromise; + return options; + } + }; + } + + function dtColumnBuilder() { + /** + * The wrapped datatables column + * @param mData the data to display of the column + * @param sTitle the sTitle of the column title to display in the DOM + */ + var DTColumn = { + /** + * Add the option of the column + * @param key the key of the option + * @param value an object or a function of the option + * @returns {DTColumn} the wrapped datatables column + */ + withOption: function(key, value) { + if (angular.isString(key)) { + this[key] = value; + } + return this; + }, + + /** + * Set the title of the colum + * @param sTitle the sTitle of the column + * @returns {DTColumn} the wrapped datatables column + */ + withTitle: function(sTitle) { + this.sTitle = sTitle; + return this; + }, + + /** + * Set the CSS class of the column + * @param sClass the CSS class + * @returns {DTColumn} the wrapped datatables column + */ + withClass: function(sClass) { + this.sClass = sClass; + return this; + }, + + /** + * Hide the column + * @returns {DTColumn} the wrapped datatables column + */ + notVisible: function() { + this.bVisible = false; + return this; + }, + + /** + * Set the column as not sortable + * @returns {DTColumn} the wrapped datatables column + */ + notSortable: function() { + this.bSortable = false; + return this; + }, + + /** + * Render each cell with the given parameter + * @mRender mRender the function/string to render the data + * @returns {DTColumn} the wrapped datatables column + */ + renderWith: function(mRender) { + this.mRender = mRender; + return this; + } + }; + + return { + /** + * Create a new wrapped datatables column + * @param mData the data of the column to display + * @param sTitle the sTitle of the column title to display in the DOM + * @returns {DTColumn} the wrapped datatables column + */ + newColumn: function(mData, sTitle) { + if (angular.isUndefined(mData)) { + throw new Error('The parameter "mData" is not defined!'); + } + var column = Object.create(DTColumn); + column.mData = mData; + if (angular.isDefined(sTitle)) { + column.sTitle = sTitle; + } + return column; + }, + DTColumn: DTColumn + }; + } + + /* @ngInject */ + function dtColumnDefBuilder(DTColumnBuilder) { + return { + newColumnDef: function(targets) { + if (angular.isUndefined(targets)) { + throw new Error('The parameter "targets" must be defined! See https://datatables.net/reference/option/columnDefs.targets'); + } + var column = Object.create(DTColumnBuilder.DTColumn); + if (angular.isArray(targets)) { + column.aTargets = targets; + } else { + column.aTargets = [targets]; + } + return column; + } + }; + } + dtColumnDefBuilder.$inject = ['DTColumnBuilder']; + + function dtLoadingTemplate($compile, DTDefaultOptions, DT_LOADING_CLASS) { + return { + compileHtml: function($scope) { + return $compile(angular.element('
      ' + DTDefaultOptions.loadingTemplate + '
      '))($scope); + }, + isLoading: function(elem) { + return elem.hasClass(DT_LOADING_CLASS); + } + }; + } + dtLoadingTemplate.$inject = ['$compile', 'DTDefaultOptions', 'DT_LOADING_CLASS']; + + 'use strict'; + + angular.module('datatables.instances', ['datatables.util']) + .factory('DTInstanceFactory', dtInstanceFactory); + + function dtInstanceFactory() { + var DTInstance = { + reloadData: reloadData, + changeData: changeData, + rerender: rerender + }; + return { + newDTInstance: newDTInstance, + copyDTProperties: copyDTProperties + }; + + function newDTInstance(renderer) { + var dtInstance = Object.create(DTInstance); + dtInstance._renderer = renderer; + return dtInstance; + } + + function copyDTProperties(result, dtInstance) { + dtInstance.id = result.id; + dtInstance.DataTable = result.DataTable; + dtInstance.dataTable = result.dataTable; + } + + function reloadData(callback, resetPaging) { + /*jshint validthis:true */ + this._renderer.reloadData(callback, resetPaging); + } + + function changeData(data) { + /*jshint validthis:true */ + this._renderer.changeData(data); + } + + function rerender() { + /*jshint validthis:true */ + this._renderer.rerender(); + } + } + + 'use strict'; + + angular.module('datatables', ['datatables.directive', 'datatables.factory']) + .run(initAngularDataTables); + + /* @ngInject */ + function initAngularDataTables() { + if ($.fn.DataTable.Api) { + /** + * Register an API to destroy a DataTable without detaching the tbody so that we can add new data + * when rendering with the "Angular way". + */ + $.fn.DataTable.Api.register('ngDestroy()', function(remove) { + remove = remove || false; + + return this.iterator('table', function(settings) { + var orig = settings.nTableWrapper.parentNode; + var classes = settings.oClasses; + var table = settings.nTable; + var tbody = settings.nTBody; + var thead = settings.nTHead; + var tfoot = settings.nTFoot; + var jqTable = $(table); + var jqTbody = $(tbody); + var jqWrapper = $(settings.nTableWrapper); + var rows = $.map(settings.aoData, function(r) { + return r.nTr; + }); + var ien; + + // Flag to note that the table is currently being destroyed - no action + // should be taken + settings.bDestroying = true; + + // Fire off the destroy callbacks for plug-ins etc + $.fn.DataTable.ext.internal._fnCallbackFire(settings, 'aoDestroyCallback', 'destroy', [settings]); + + // If not being removed from the document, make all columns visible + if (!remove) { + new $.fn.DataTable.Api(settings).columns().visible(true); + } + + // Blitz all `DT` namespaced events (these are internal events, the + // lowercase, `dt` events are user subscribed and they are responsible + // for removing them + jqWrapper.unbind('.DT').find(':not(tbody *)').unbind('.DT'); + $(window).unbind('.DT-' + settings.sInstance); + + // When scrolling we had to break the table up - restore it + if (table !== thead.parentNode) { + jqTable.children('thead').detach(); + jqTable.append(thead); + } + + if (tfoot && table !== tfoot.parentNode) { + jqTable.children('tfoot').detach(); + jqTable.append(tfoot); + } + + // Remove the DataTables generated nodes, events and classes + jqTable.detach(); + jqWrapper.detach(); + + settings.aaSorting = []; + settings.aaSortingFixed = []; + $.fn.DataTable.ext.internal._fnSortingClasses(settings); + + $(rows).removeClass(settings.asStripeClasses.join(' ')); + + $('th, td', thead).removeClass(classes.sSortable + ' ' + + classes.sSortableAsc + ' ' + classes.sSortableDesc + ' ' + classes.sSortableNone + ); + + if (settings.bJUI) { + $('th span.' + classes.sSortIcon + ', td span.' + classes.sSortIcon, thead).detach(); + $('th, td', thead).each(function() { + var wrapper = $('div.' + classes.sSortJUIWrapper, this); + $(this).append(wrapper.contents()); + wrapper.detach(); + }); + } + + // ------------------------------------------------------------------------- + // This is the only change with the "destroy()" API (with DT v1.10.1) + // ------------------------------------------------------------------------- + if (!remove && orig) { + // insertBefore acts like appendChild if !arg[1] + if (orig.contains(settings.nTableReinsertBefore)) { + orig.insertBefore(table, settings.nTableReinsertBefore); + } else { + orig.appendChild(table); + } + } + // Add the TR elements back into the table in their original order + // jqTbody.children().detach(); + // jqTbody.append( rows ); + // ------------------------------------------------------------------------- + + // Restore the width of the original table - was read from the style property, + // so we can restore directly to that + jqTable + .css('width', settings.sDestroyWidth) + .removeClass(classes.sTable); + + // If the were originally stripe classes - then we add them back here. + // Note this is not fool proof (for example if not all rows had stripe + // classes - but it's a good effort without getting carried away + ien = settings.asDestroyStripes.length; + + if (ien) { + jqTbody.children().each(function(i) { + $(this).addClass(settings.asDestroyStripes[i % ien]); + }); + } + + /* Remove the settings object from the settings array */ + var idx = $.inArray(settings, $.fn.DataTable.settings); + if (idx !== -1) { + $.fn.DataTable.settings.splice(idx, 1); + } + }); + }); + } + } + + 'use strict'; + angular.module('datatables.options', []) + .constant('DT_DEFAULT_OPTIONS', { + // Default ajax properties. See http://legacy.datatables.net/usage/options#sAjaxDataProp + sAjaxDataProp: '', + // Set default columns (used when none are provided) + aoColumns: [] + }) + .constant('DT_LOADING_CLASS', 'dt-loading') + .service('DTDefaultOptions', dtDefaultOptions); + + function dtDefaultOptions() { + var options = { + loadingTemplate: '

      Loading...

      ', + bootstrapOptions: {}, + setLoadingTemplate: setLoadingTemplate, + setLanguageSource: setLanguageSource, + setLanguage: setLanguage, + setDisplayLength: setDisplayLength, + setBootstrapOptions: setBootstrapOptions, + setDOM: setDOM + }; + + return options; + + /** + * Set the default loading template + * @param loadingTemplate the HTML to display when loading the table + * @returns {DTDefaultOptions} the default option config + */ + function setLoadingTemplate(loadingTemplate) { + options.loadingTemplate = loadingTemplate; + return options; + } + + /** + * Set the default language source for all datatables + * @param sLanguageSource the language source + * @returns {DTDefaultOptions} the default option config + */ + function setLanguageSource(sLanguageSource) { + // HACK to resolve the language source manually instead of DT + // See https://github.com/l-lin/angular-datatables/issues/356 + $.ajax({ + dataType: 'json', + url: sLanguageSource, + success: function(json) { + $.extend(true, $.fn.DataTable.defaults, { + language: json + }); + } + }); + return options; + } + + /** + * Set the language for all datatables + * @param language the language + * @returns {DTDefaultOptions} the default option config + */ + function setLanguage(language) { + $.extend(true, $.fn.DataTable.defaults, { + language: language + }); + return options; + } + + /** + * Set the default number of items to display for all datatables + * @param displayLength the number of items to display + * @returns {DTDefaultOptions} the default option config + */ + function setDisplayLength(displayLength) { + $.extend($.fn.DataTable.defaults, { + displayLength: displayLength + }); + return options; + } + + /** + * Set the default options to be use for Bootstrap integration. + * See https://github.com/l-lin/angular-datatables/blob/dev/src/angular-datatables.bootstrap.options.js to check + * what default options Angular DataTables is using. + * @param oBootstrapOptions an object containing the default options for Bootstrap integration + * @returns {DTDefaultOptions} the default option config + */ + function setBootstrapOptions(oBootstrapOptions) { + options.bootstrapOptions = oBootstrapOptions; + return options; + } + + /** + * Set the DOM for all DataTables. + * See https://datatables.net/reference/option/dom + * @param dom the dom + * @returns {DTDefaultoptions} the default option config + */ + function setDOM(dom) { + $.extend($.fn.DataTable.defaults, { + dom: dom + }); + return options; + } + } + + 'use strict'; + angular.module('datatables.renderer', ['datatables.instances', 'datatables.factory', 'datatables.options', 'datatables.instances']) + .factory('DTRendererService', dtRendererService) + .factory('DTRenderer', dtRenderer) + .factory('DTDefaultRenderer', dtDefaultRenderer) + .factory('DTNGRenderer', dtNGRenderer) + .factory('DTPromiseRenderer', dtPromiseRenderer) + .factory('DTAjaxRenderer', dtAjaxRenderer) + .factory('DTRendererFactory', dtRendererFactory); + + /* @ngInject */ + function dtRendererService(DTLoadingTemplate) { + var plugins = []; + var rendererService = { + showLoading: showLoading, + hideLoading: hideLoading, + renderDataTable: renderDataTable, + hideLoadingAndRenderDataTable: hideLoadingAndRenderDataTable, + registerPlugin: registerPlugin, + postRender: postRender, + preRender: preRender + }; + return rendererService; + + function showLoading($elem, $scope) { + var $loading = angular.element(DTLoadingTemplate.compileHtml($scope)); + $elem.after($loading); + $elem.hide(); + $loading.show(); + } + + function hideLoading($elem) { + $elem.show(); + var next = $elem.next(); + if (DTLoadingTemplate.isLoading(next)) { + next.remove(); + } + } + + function renderDataTable($elem, options) { + var dtId = '#' + $elem.attr('id'); + if ($.fn.dataTable.isDataTable(dtId) && angular.isObject(options)) { + options.destroy = true; + } + // See http://datatables.net/manual/api#Accessing-the-API to understand the difference between DataTable and dataTable + var DT = $elem.DataTable(options); + var dt = $elem.dataTable(); + + var result = { + id: $elem.attr('id'), + DataTable: DT, + dataTable: dt + }; + + postRender(options, result); + + return result; + } + + function hideLoadingAndRenderDataTable($elem, options) { + rendererService.hideLoading($elem); + return rendererService.renderDataTable($elem, options); + } + + function registerPlugin(plugin) { + plugins.push(plugin); + } + + function postRender(options, result) { + angular.forEach(plugins, function(plugin) { + if (angular.isFunction(plugin.postRender)) { + plugin.postRender(options, result); + } + }); + } + + function preRender(options) { + angular.forEach(plugins, function(plugin) { + if (angular.isFunction(plugin.preRender)) { + plugin.preRender(options); + } + }); + } + } + dtRendererService.$inject = ['DTLoadingTemplate']; + + function dtRenderer() { + return { + withOptions: function(options) { + this.options = options; + return this; + } + }; + } + + /* @ngInject */ + function dtDefaultRenderer($q, DTRenderer, DTRendererService, DTInstanceFactory) { + return { + create: create + }; + + function create(options) { + var _oTable; + var _$elem; + var _$scope; + var renderer = Object.create(DTRenderer); + renderer.name = 'DTDefaultRenderer'; + renderer.options = options; + renderer.render = render; + renderer.reloadData = reloadData; + renderer.changeData = changeData; + renderer.rerender = rerender; + + function render($elem, $scope) { + _$elem = $elem; + _$scope = $scope; + var dtInstance = DTInstanceFactory.newDTInstance(renderer); + var result = DTRendererService.hideLoadingAndRenderDataTable($elem, renderer.options); + _oTable = result.DataTable; + DTInstanceFactory.copyDTProperties(result, dtInstance); + return $q.when(dtInstance); + } + + function reloadData() { + // Do nothing + } + + function changeData() { + // Do nothing + } + + function rerender() { + _oTable.destroy(); + DTRendererService.showLoading(_$elem, _$scope); + render(_$elem, _$scope); + } + return renderer; + } + } + dtDefaultRenderer.$inject = ['$q', 'DTRenderer', 'DTRendererService', 'DTInstanceFactory']; + + /* @ngInject */ + function dtNGRenderer($log, $q, $compile, $timeout, DTRenderer, DTRendererService, DTInstanceFactory) { + /** + * Renderer for displaying the Angular way + * @param options + * @returns {{options: *}} the renderer + * @constructor + */ + return { + create: create + }; + + function create(options) { + var _staticHTML; + var _oTable; + var _$elem; + var _parentScope; + var _newParentScope; + var dtInstance; + var renderer = Object.create(DTRenderer); + renderer.name = 'DTNGRenderer'; + renderer.options = options; + renderer.render = render; + renderer.reloadData = reloadData; + renderer.changeData = changeData; + renderer.rerender = rerender; + return renderer; + + function render($elem, $scope, staticHTML) { + _staticHTML = staticHTML; + _$elem = $elem; + _parentScope = $scope.$parent; + dtInstance = DTInstanceFactory.newDTInstance(renderer); + + var defer = $q.defer(); + var _expression = $elem.find('tbody').html(); + // Find the resources from the comment displayed by angular in the DOM + // This regexp is inspired by the one used in the "ngRepeat" directive + var _match = _expression.match(/^\s*.+?\s+in\s+(\S*)\s*/m); + + if (!_match) { + throw new Error('Expected expression in form of "_item_ in _collection_[ track by _id_]" but got "{0}".', _expression); + } + var _ngRepeatAttr = _match[1]; + + var _alreadyRendered = false; + + _parentScope.$watchCollection(_ngRepeatAttr, function() { + if (_oTable && _alreadyRendered) { + _destroyAndCompile(); + } + $timeout(function() { + _alreadyRendered = true; + // Ensure that prerender is called when the collection is updated + // See https://github.com/l-lin/angular-datatables/issues/502 + DTRendererService.preRender(renderer.options); + var result = DTRendererService.hideLoadingAndRenderDataTable(_$elem, renderer.options); + _oTable = result.DataTable; + DTInstanceFactory.copyDTProperties(result, dtInstance); + defer.resolve(dtInstance); + }, 0, false); + }, true); + return defer.promise; + } + + function reloadData() { + $log.warn('The Angular Renderer does not support reloading data. You need to do it directly on your model'); + } + + function changeData() { + $log.warn('The Angular Renderer does not support changing the data. You need to change your model directly.'); + } + + function rerender() { + _destroyAndCompile(); + DTRendererService.showLoading(_$elem, _parentScope); + // Ensure that prerender is called after loadData from promise + // See https://github.com/l-lin/angular-datatables/issues/563 + DTRendererService.preRender(options); + $timeout(function() { + var result = DTRendererService.hideLoadingAndRenderDataTable(_$elem, renderer.options); + _oTable = result.DataTable; + DTInstanceFactory.copyDTProperties(result, dtInstance); + }, 0, false); + } + + function _destroyAndCompile() { + if (_newParentScope) { + _newParentScope.$destroy(); + } + _oTable.ngDestroy(); + // Re-compile because we lost the angular binding to the existing data + _$elem.html(_staticHTML); + _newParentScope = _parentScope.$new(); + $compile(_$elem.contents())(_newParentScope); + } + } + } + dtNGRenderer.$inject = ['$log', '$q', '$compile', '$timeout', 'DTRenderer', 'DTRendererService', 'DTInstanceFactory']; + + /* @ngInject */ + function dtPromiseRenderer($q, $timeout, $log, DTRenderer, DTRendererService, DTInstanceFactory) { + /** + * Renderer for displaying with a promise + * @param options the options + * @returns {{options: *}} the renderer + * @constructor + */ + return { + create: create + }; + + function create(options) { + var _oTable; + var _loadedPromise = null; + var _$elem; + var _$scope; + + var dtInstance; + var renderer = Object.create(DTRenderer); + renderer.name = 'DTPromiseRenderer'; + renderer.options = options; + renderer.render = render; + renderer.reloadData = reloadData; + renderer.changeData = changeData; + renderer.rerender = rerender; + return renderer; + + function render($elem, $scope) { + var defer = $q.defer(); + dtInstance = DTInstanceFactory.newDTInstance(renderer); + _$elem = $elem; + _$scope = $scope; + _resolve(renderer.options.fnPromise, DTRendererService.renderDataTable).then(function(result) { + _oTable = result.DataTable; + DTInstanceFactory.copyDTProperties(result, dtInstance); + defer.resolve(dtInstance); + }); + return defer.promise; + } + + function reloadData(callback, resetPaging) { + var previousPage = _oTable && _oTable.page() ? _oTable.page() : 0; + if (angular.isFunction(renderer.options.fnPromise)) { + _resolve(renderer.options.fnPromise, _redrawRows).then(function(result) { + if (angular.isFunction(callback)) { + callback(result.DataTable.data()); + } + if (resetPaging === false) { + result.DataTable.page(previousPage).draw(false); + } + }); + } else { + $log.warn('In order to use the reloadData functionality with a Promise renderer, you need to provide a function that returns a promise.'); + } + } + + function changeData(fnPromise) { + renderer.options.fnPromise = fnPromise; + // We also need to set the $scope.dtOptions, otherwise, when we change the columns, it will revert to the old data + // See https://github.com/l-lin/angular-datatables/issues/359 + _$scope.dtOptions.fnPromise = fnPromise; + _resolve(renderer.options.fnPromise, _redrawRows); + } + + function rerender() { + _oTable.destroy(); + DTRendererService.showLoading(_$elem, _$scope); + // Ensure that prerender is called after loadData from promise + // See https://github.com/l-lin/angular-datatables/issues/563 + DTRendererService.preRender(options); + render(_$elem, _$scope); + } + + function _resolve(fnPromise, callback) { + var defer = $q.defer(); + if (angular.isUndefined(fnPromise)) { + throw new Error('You must provide a promise or a function that returns a promise!'); + } + if (_loadedPromise) { + _loadedPromise.then(function() { + defer.resolve(_startLoading(fnPromise, callback)); + }); + } else { + defer.resolve(_startLoading(fnPromise, callback)); + } + return defer.promise; + } + + function _startLoading(fnPromise, callback) { + var defer = $q.defer(); + if (angular.isFunction(fnPromise)) { + _loadedPromise = fnPromise(); + } else { + _loadedPromise = fnPromise; + } + _loadedPromise.then(function(result) { + var data = result; + // In case the data is nested in an object + if (renderer.options.sAjaxDataProp) { + var properties = renderer.options.sAjaxDataProp.split('.'); + while (properties.length) { + var property = properties.shift(); + if (property in data) { + data = data[property]; + } + } + } + _loadedPromise = null; + defer.resolve(_doRender(renderer.options, _$elem, data, callback)); + }); + return defer.promise; + } + + function _doRender(options, $elem, data, callback) { + var defer = $q.defer(); + // Since Angular 1.3, the promise renderer is throwing "Maximum call stack size exceeded" + // By removing the $promise attribute, we avoid an infinite loop when jquery is cloning the data + // See https://github.com/l-lin/angular-datatables/issues/110 + delete data.$promise; + options.aaData = data; + // Add $timeout to be sure that angular has finished rendering before calling datatables + $timeout(function() { + DTRendererService.hideLoading($elem); + // Set it to true in order to be able to redraw the dataTable + options.bDestroy = true; + defer.resolve(callback($elem, options)); + }, 0, false); + return defer.promise; + } + + function _redrawRows($elem, options) { + _oTable.clear(); + _oTable.rows.add(options.aaData).draw(options.redraw); + return { + id: dtInstance.id, + DataTable: dtInstance.DataTable, + dataTable: dtInstance.dataTable + }; + } + } + } + dtPromiseRenderer.$inject = ['$q', '$timeout', '$log', 'DTRenderer', 'DTRendererService', 'DTInstanceFactory']; + + /* @ngInject */ + function dtAjaxRenderer($q, $timeout, DTRenderer, DTRendererService, DT_DEFAULT_OPTIONS, DTInstanceFactory) { + /** + * Renderer for displaying with Ajax + * @param options the options + * @returns {{options: *}} the renderer + * @constructor + */ + return { + create: create + }; + + function create(options) { + var _oTable; + var _$elem; + var _$scope; + var renderer = Object.create(DTRenderer); + renderer.name = 'DTAjaxRenderer'; + renderer.options = options; + renderer.render = render; + renderer.reloadData = reloadData; + renderer.changeData = changeData; + renderer.rerender = rerender; + return renderer; + + function render($elem, $scope) { + _$elem = $elem; + _$scope = $scope; + var defer = $q.defer(); + var dtInstance = DTInstanceFactory.newDTInstance(renderer); + // Define default values in case it is an ajax datatables + if (angular.isUndefined(renderer.options.sAjaxDataProp)) { + renderer.options.sAjaxDataProp = DT_DEFAULT_OPTIONS.sAjaxDataProp; + } + if (angular.isUndefined(renderer.options.aoColumns)) { + renderer.options.aoColumns = DT_DEFAULT_OPTIONS.aoColumns; + } + _doRender(renderer.options, $elem).then(function(result) { + _oTable = result.DataTable; + DTInstanceFactory.copyDTProperties(result, dtInstance); + defer.resolve(dtInstance); + }); + return defer.promise; + } + + function reloadData(callback, resetPaging) { + if (_oTable) { + _oTable.ajax.reload(callback, resetPaging); + } + } + + function changeData(ajax) { + renderer.options.ajax = ajax; + // We also need to set the $scope.dtOptions, otherwise, when we change the columns, it will revert to the old data + // See https://github.com/l-lin/angular-datatables/issues/359 + _$scope.dtOptions.ajax = ajax; + } + + function rerender() { + // Ensure that prerender is called after loadData from promise + // See https://github.com/l-lin/angular-datatables/issues/563 + DTRendererService.preRender(options); + render(_$elem, _$scope); + } + + function _doRender(options, $elem) { + var defer = $q.defer(); + // Destroy the table if it exists in order to be able to redraw the dataTable + options.bDestroy = true; + if (_oTable) { + _oTable.destroy(); + DTRendererService.showLoading(_$elem, _$scope); + // Empty in case of columns change + $elem.empty(); + } + DTRendererService.hideLoading($elem); + // Condition to refresh the dataTable + if (_shouldDeferRender(options)) { + $timeout(function() { + defer.resolve(DTRendererService.renderDataTable($elem, options)); + }, 0, false); + } else { + defer.resolve(DTRendererService.renderDataTable($elem, options)); + } + return defer.promise; + } + // See https://github.com/l-lin/angular-datatables/issues/147 + function _shouldDeferRender(options) { + if (angular.isDefined(options) && angular.isDefined(options.dom)) { + // S for scroller plugin + return options.dom.indexOf('S') >= 0; + } + return false; + } + } + } + dtAjaxRenderer.$inject = ['$q', '$timeout', 'DTRenderer', 'DTRendererService', 'DT_DEFAULT_OPTIONS', 'DTInstanceFactory']; + + /* @ngInject */ + function dtRendererFactory(DTDefaultRenderer, DTNGRenderer, DTPromiseRenderer, DTAjaxRenderer) { + return { + fromOptions: fromOptions + }; + + function fromOptions(options, isNgDisplay) { + if (isNgDisplay) { + if (options && options.serverSide) { + throw new Error('You cannot use server side processing along with the Angular renderer!'); + } + return DTNGRenderer.create(options); + } + if (angular.isDefined(options)) { + if (angular.isDefined(options.fnPromise) && options.fnPromise !== null) { + if (options.serverSide) { + throw new Error('You cannot use server side processing along with the Promise renderer!'); + } + return DTPromiseRenderer.create(options); + } + if (angular.isDefined(options.ajax) && options.ajax !== null || + angular.isDefined(options.ajax) && options.ajax !== null) { + return DTAjaxRenderer.create(options); + } + return DTDefaultRenderer.create(options); + } + return DTDefaultRenderer.create(); + } + } + dtRendererFactory.$inject = ['DTDefaultRenderer', 'DTNGRenderer', 'DTPromiseRenderer', 'DTAjaxRenderer']; + + 'use strict'; + + angular.module('datatables.util', []) + .factory('DTPropertyUtil', dtPropertyUtil); + + /* @ngInject */ + function dtPropertyUtil($q) { + return { + overrideProperties: overrideProperties, + deleteProperty: deleteProperty, + resolveObjectPromises: resolveObjectPromises, + resolveArrayPromises: resolveArrayPromises + }; + + /** + * Overrides the source property with the given target properties. + * Source is not written. It's making a fresh copy of it in order to ensure that we do not change the parameters. + * @param source the source properties to override + * @param target the target properties + * @returns {*} the object overrided + */ + function overrideProperties(source, target) { + var result = angular.copy(source); + + if (angular.isUndefined(result) || result === null) { + result = {}; + } + if (angular.isUndefined(target) || target === null) { + return result; + } + if (angular.isObject(target)) { + for (var prop in target) { + if (target.hasOwnProperty(prop)) { + result[prop] = overrideProperties(result[prop], target[prop]); + } + } + } else { + result = angular.copy(target); + } + return result; + } + + /** + * Delete the property from the given object + * @param obj the object + * @param propertyName the property name + */ + function deleteProperty(obj, propertyName) { + if (angular.isObject(obj)) { + delete obj[propertyName]; + } + } + + /** + * Resolve any promises from a given object if there are any. + * @param obj the object + * @param excludedPropertiesName the list of properties to ignore + * @returns {promise} the promise that the object attributes promises are all resolved + */ + function resolveObjectPromises(obj, excludedPropertiesName) { + var defer = $q.defer(), + promises = [], + resolvedObj = {}, + excludedProp = excludedPropertiesName || []; + if (!angular.isObject(obj) || angular.isArray(obj)) { + defer.resolve(obj); + } else { + resolvedObj = angular.extend(resolvedObj, obj); + for (var prop in resolvedObj) { + if (resolvedObj.hasOwnProperty(prop) && $.inArray(prop, excludedProp) === -1) { + if (angular.isArray(resolvedObj[prop])) { + promises.push(resolveArrayPromises(resolvedObj[prop])); + } else { + promises.push($q.when(resolvedObj[prop])); + } + } + } + $q.all(promises).then(function(result) { + var index = 0; + for (var prop in resolvedObj) { + if (resolvedObj.hasOwnProperty(prop) && $.inArray(prop, excludedProp) === -1) { + resolvedObj[prop] = result[index++]; + } + } + defer.resolve(resolvedObj); + }); + } + return defer.promise; + } + + /** + * Resolve the given array promises + * @param array the array containing promise or not + * @returns {promise} the promise that the array contains a list of objects/values promises that are resolved + */ + function resolveArrayPromises(array) { + var defer = $q.defer(), + promises = [], + resolveArray = []; + if (!angular.isArray(array)) { + defer.resolve(array); + } else { + angular.forEach(array, function(item) { + if (angular.isObject(item)) { + promises.push(resolveObjectPromises(item)); + } else { + promises.push($q.when(item)); + } + }); + $q.all(promises).then(function(result) { + angular.forEach(result, function(item) { + resolveArray.push(item); + }); + defer.resolve(resolveArray); + }); + } + return defer.promise; + } + } + dtPropertyUtil.$inject = ['$q']; + + +})(window, document, jQuery, angular); + +/*! + * angular-datatables - v0.5.4 + * https://github.com/l-lin/angular-datatables + * License: MIT + */ +if (typeof module !== "undefined" && typeof exports !== "undefined" && module.exports === exports) { + module.exports = 'datatables.light-columnfilter'; +} +(function(window, document, $, angular) { + + 'use strict'; + + // See https://github.com/thansen-solire/datatables-light-columnfilter + angular.module('datatables.light-columnfilter', ['datatables']) + .config(dtLightColumnFilterConfig) + .run(initLightColumnFilterPlugin); + + /* @ngInject */ + function dtLightColumnFilterConfig($provide) { + $provide.decorator('DTOptionsBuilder', dtOptionsBuilderDecorator); + + function dtOptionsBuilderDecorator($delegate) { + var newOptions = $delegate.newOptions; + var fromSource = $delegate.fromSource; + var fromFnPromise = $delegate.fromFnPromise; + + $delegate.newOptions = function() { + return _decorateOptions(newOptions); + }; + $delegate.fromSource = function(ajax) { + return _decorateOptions(fromSource, ajax); + }; + $delegate.fromFnPromise = function(fnPromise) { + return _decorateOptions(fromFnPromise, fnPromise); + }; + + return $delegate; + + function _decorateOptions(fn, params) { + var options = fn(params); + options.withLightColumnFilter = withLightColumnFilter; + return options; + + /** + * Add column filter support + * @param lightColumnFilterOptions the plugins options + * @returns {DTOptions} the options + */ + function withLightColumnFilter(lightColumnFilterOptions) { + options.hasLightColumnFilter = true; + if (lightColumnFilterOptions) { + options.lightColumnFilterOptions = lightColumnFilterOptions; + } + return options; + } + } + } + dtOptionsBuilderDecorator.$inject = ['$delegate']; + } + dtLightColumnFilterConfig.$inject = ['$provide']; + + /* @ngInject */ + function initLightColumnFilterPlugin(DTRendererService) { + var lightColumnFilterPlugin = { + postRender: postRender + }; + DTRendererService.registerPlugin(lightColumnFilterPlugin); + + function postRender(options, result) { + if (options && options.hasLightColumnFilter) { + new $.fn.dataTable.ColumnFilter(result.DataTable, options.lightColumnFilterOptions); + } + } + } + initLightColumnFilterPlugin.$inject = ['DTRendererService']; + + +})(window, document, jQuery, angular); + +/*! + * angular-datatables - v0.5.4 + * https://github.com/l-lin/angular-datatables + * License: MIT + */ +if (typeof module !== "undefined" && typeof exports !== "undefined" && module.exports === exports) { + module.exports = 'datatables.scroller'; +} +(function(window, document, $, angular) { + + 'use strict'; + + // See http://datatables.net/extensions/scroller/ + angular.module('datatables.scroller', ['datatables']) + .config(dtScrollerConfig); + + /* @ngInject */ + function dtScrollerConfig($provide) { + $provide.decorator('DTOptionsBuilder', dtOptionsBuilderDecorator); + + function dtOptionsBuilderDecorator($delegate) { + var newOptions = $delegate.newOptions; + var fromSource = $delegate.fromSource; + var fromFnPromise = $delegate.fromFnPromise; + + $delegate.newOptions = function() { + return _decorateOptions(newOptions); + }; + $delegate.fromSource = function(ajax) { + return _decorateOptions(fromSource, ajax); + }; + $delegate.fromFnPromise = function(fnPromise) { + return _decorateOptions(fromFnPromise, fnPromise); + }; + + return $delegate; + + function _decorateOptions(fn, params) { + var options = fn(params); + options.withScroller = withScroller; + return options; + + /** + * Add scroller compatibility + * @returns {DTOptions} the options + */ + function withScroller() { + var scrollerSuffix = 'S'; + options.dom = options.dom ? options.dom : $.fn.dataTable.defaults.sDom; + if (options.dom.indexOf(scrollerSuffix) === -1) { + options.dom = options.dom + scrollerSuffix; + } + return options; + } + } + } + dtOptionsBuilderDecorator.$inject = ['$delegate']; + } + dtScrollerConfig.$inject = ['$provide']; + + +})(window, document, jQuery, angular); + +/*! + * angular-datatables - v0.5.4 + * https://github.com/l-lin/angular-datatables + * License: MIT + */ +if (typeof module !== "undefined" && typeof exports !== "undefined" && module.exports === exports) { + module.exports = 'datatables.select'; +} +(function(window, document, $, angular) { + + 'use strict'; + + // See https://datatables.net/extensions/select/ + angular.module('datatables.select', ['datatables']) + .config(dtSelectConfig); + + /* @ngInject */ + function dtSelectConfig($provide) { + $provide.decorator('DTOptionsBuilder', dtOptionsBuilderDecorator); + + function dtOptionsBuilderDecorator($delegate) { + var newOptions = $delegate.newOptions; + var fromSource = $delegate.fromSource; + var fromFnPromise = $delegate.fromFnPromise; + + $delegate.newOptions = function() { + return _decorateOptions(newOptions); + }; + $delegate.fromSource = function(ajax) { + return _decorateOptions(fromSource, ajax); + }; + $delegate.fromFnPromise = function(fnPromise) { + return _decorateOptions(fromFnPromise, fnPromise); + }; + + return $delegate; + + function _decorateOptions(fn, params) { + var options = fn(params); + options.withSelect = withSelect; + return options; + + /** + * Add select compatibility + * @param selectOptions the options of the select extension (see https://datatables.net/reference/option/#select) + * @returns {DTOptions} the options + */ + function withSelect(selectOptions) { + if (angular.isUndefined(selectOptions)) { + throw new Error('You must define the options for the select extension. See https://datatables.net/reference/option/#select'); + } + options.select = selectOptions; + return options; + } + } + } + dtOptionsBuilderDecorator.$inject = ['$delegate']; + } + dtSelectConfig.$inject = ['$provide']; + + +})(window, document, jQuery, angular); + +/*! + * angular-datatables - v0.5.4 + * https://github.com/l-lin/angular-datatables + * License: MIT + */ +if (typeof module !== "undefined" && typeof exports !== "undefined" && module.exports === exports) { + module.exports = 'datatables.tabletools'; +} +(function(window, document, $, angular) { + + 'use strict'; + + // See https://datatables.net/extras/tabletools/ + angular.module('datatables.tabletools', ['datatables']) + .config(dtTableToolsConfig); + + /* @ngInject */ + function dtTableToolsConfig($provide) { + $provide.decorator('DTOptionsBuilder', dtOptionsBuilderDecorator); + + function dtOptionsBuilderDecorator($delegate) { + var newOptions = $delegate.newOptions; + var fromSource = $delegate.fromSource; + var fromFnPromise = $delegate.fromFnPromise; + + $delegate.newOptions = function() { + return _decorateOptions(newOptions); + }; + $delegate.fromSource = function(ajax) { + return _decorateOptions(fromSource, ajax); + }; + $delegate.fromFnPromise = function(fnPromise) { + return _decorateOptions(fromFnPromise, fnPromise); + }; + + return $delegate; + + function _decorateOptions(fn, params) { + var options = fn(params); + options.withTableTools = withTableTools; + options.withTableToolsOption = withTableToolsOption; + options.withTableToolsButtons = withTableToolsButtons; + return options; + + /** + * Add table tools compatibility + * @param sSwfPath the path to the swf file to export in csv/xls + * @returns {DTOptions} the options + */ + function withTableTools(sSwfPath) { + console.warn('The tabletools extension has been retired. Please use the select and buttons extensions instead: https://datatables.net/extensions/select/ and https://datatables.net/extensions/buttons/'); + var tableToolsPrefix = 'T'; + options.dom = options.dom ? options.dom : $.fn.dataTable.defaults.sDom; + if (options.dom.indexOf(tableToolsPrefix) === -1) { + options.dom = tableToolsPrefix + options.dom; + } + options.hasTableTools = true; + if (angular.isString(sSwfPath)) { + options.withTableToolsOption('sSwfPath', sSwfPath); + } + return options; + } + + /** + * Add option to "oTableTools" option + * @param key the key of the option to add + * @param value an object or a function of the function + * @returns {DTOptions} the options + */ + function withTableToolsOption(key, value) { + if (angular.isString(key)) { + options.oTableTools = options.oTableTools && options.oTableTools !== null ? options.oTableTools : {}; + options.oTableTools[key] = value; + } + return options; + } + + /** + * Set the table tools buttons to display + * @param aButtons the array of buttons to display + * @returns {DTOptions} the options + */ + function withTableToolsButtons(aButtons) { + if (angular.isArray(aButtons)) { + options.withTableToolsOption('aButtons', aButtons); + } + return options; + } + } + } + dtOptionsBuilderDecorator.$inject = ['$delegate']; + } + dtTableToolsConfig.$inject = ['$provide']; + + +})(window, document, jQuery, angular); + +/** + * Bunch of useful filters for angularJS(with no external dependencies!) + * @version v0.5.16 - 2017-04-07 * @link https://github.com/a8m/angular-filter + * @author Ariel Mashraki + * @license MIT License, http://www.opensource.org/licenses/MIT + */ +(function ( window, angular, undefined ) { +/*jshint globalstrict:true*/ +'use strict'; + +var isDefined = angular.isDefined, + isUndefined = angular.isUndefined, + isFunction = angular.isFunction, + isString = angular.isString, + isNumber = angular.isNumber, + isObject = angular.isObject, + isArray = angular.isArray, + forEach = angular.forEach, + extend = angular.extend, + copy = angular.copy, + equals = angular.equals; + + +/** + * @description + * get an object and return array of values + * @param object + * @returns {Array} + */ +function toArray(object) { + return isArray(object) + ? object + : Object.keys(object).map(function(key) { + return object[key]; + }); +} + +/** + * @param value + * @returns {boolean} + */ +function isNull(value) { + return value === null; +} + +/** + * @description + * return if object contains partial object + * @param partial{object} + * @param object{object} + * @returns {boolean} + */ +function objectContains(partial, object) { + var keys = Object.keys(partial); + + return keys.map(function(el) { + return (object[el] !== undefined) && (object[el] == partial[el]); + }).indexOf(false) == -1; + +} + +/** + * @description + * search for approximate pattern in string + * @param word + * @param pattern + * @returns {*} + */ +function hasApproxPattern(word, pattern) { + // cheaper version of indexOf; instead of creating each + // iteration new str. + function indexOf(word, p, c) { + var j = 0; + while ((p + j) <= word.length) { + if (word.charAt(p + j) == c) return j; + j++; + } + return -1; + } + var p = 0; + for (var i = 0; i <= pattern.length; i++) { + var index = indexOf(word, p, pattern.charAt(i)); + if (index == -1) return false; + p += index + 1; + } + return true +} + +/** + * @description + * return the first n element of an array, + * if expression provided, is returns as long the expression return truthy + * @param array + * @param n {number} + * @param expression {$parse} + * @return array or single object + */ +function getFirstMatches(array, n, expression) { + var count = 0; + + return array.filter(function(elm) { + var rest = isDefined(expression) ? (count < n && expression(elm)) : count < n; + count = rest ? count+1 : count; + + return rest; + }); +} +/** + * Polyfill to ECMA6 String.prototype.contains + */ +if (!String.prototype.contains) { + String.prototype.contains = function() { + return String.prototype.indexOf.apply(this, arguments) !== -1; + }; +} + +/** + * @param num {Number} + * @param decimal {Number} + * @returns {Number} + */ +function convertToDecimal(num, decimal){ + return Math.round(num * Math.pow(10,decimal)) / (Math.pow(10, decimal)); +} + +/** + * @description + * Get an object, and return an array composed of it's properties names(nested too). + * @param obj {Object} + * @param stack {Array} + * @param parent {String} + * @returns {Array} + * @example + * parseKeys({ a:1, b: { c:2, d: { e: 3 } } }) ==> ["a", "b.c", "b.d.e"] + */ +function deepKeys(obj, stack, parent) { + stack = stack || []; + var keys = Object.keys(obj); + + keys.forEach(function(el) { + //if it's a nested object + if(isObject(obj[el]) && !isArray(obj[el])) { + //concatenate the new parent if exist + var p = parent ? parent + '.' + el : parent; + deepKeys(obj[el], stack, p || el); + } else { + //create and save the key + var key = parent ? parent + '.' + el : el; + stack.push(key) + } + }); + return stack +} + +/** + * @description + * Test if given object is a Scope instance + * @param obj + * @returns {Boolean} + */ +function isScope(obj) { + return obj && obj.$evalAsync && obj.$watch; +} + +/** + * @ngdoc filter + * @name a8m.angular + * @kind function + * + * @description + * reference to angular function + */ + +angular.module('a8m.angular', []) + + .filter('isUndefined', function () { + return function (input) { + return angular.isUndefined(input); + } + }) + .filter('isDefined', function() { + return function (input) { + return angular.isDefined(input); + } + }) + .filter('isFunction', function() { + return function (input) { + return angular.isFunction(input); + } + }) + .filter('isString', function() { + return function (input) { + return angular.isString(input) + } + }) + .filter('isNumber', function() { + return function (input) { + return angular.isNumber(input); + } + }) + .filter('isArray', function() { + return function (input) { + return angular.isArray(input); + } + }) + .filter('isObject', function() { + return function (input) { + return angular.isObject(input); + } + }) + .filter('isEqual', function() { + return function (o1, o2) { + return angular.equals(o1, o2); + } + }); + +/** + * @ngdoc filter + * @name a8m.conditions + * @kind function + * + * @description + * reference to math conditions + */ + angular.module('a8m.conditions', []) + + .filter({ + isGreaterThan : isGreaterThanFilter, + '>' : isGreaterThanFilter, + + isGreaterThanOrEqualTo : isGreaterThanOrEqualToFilter, + '>=' : isGreaterThanOrEqualToFilter, + + isLessThan : isLessThanFilter, + '<' : isLessThanFilter, + + isLessThanOrEqualTo : isLessThanOrEqualToFilter, + '<=' : isLessThanOrEqualToFilter, + + isEqualTo : isEqualToFilter, + '==' : isEqualToFilter, + + isNotEqualTo : isNotEqualToFilter, + '!=' : isNotEqualToFilter, + + isIdenticalTo : isIdenticalToFilter, + '===' : isIdenticalToFilter, + + isNotIdenticalTo : isNotIdenticalToFilter, + '!==' : isNotIdenticalToFilter + }); + + function isGreaterThanFilter() { + return function (input, check) { + return input > check; + }; + } + + function isGreaterThanOrEqualToFilter() { + return function (input, check) { + return input >= check; + }; + } + + function isLessThanFilter() { + return function (input, check) { + return input < check; + }; + } + + function isLessThanOrEqualToFilter() { + return function (input, check) { + return input <= check; + }; + } + + function isEqualToFilter() { + return function (input, check) { + return input == check; + }; + } + + function isNotEqualToFilter() { + return function (input, check) { + return input != check; + }; + } + + function isIdenticalToFilter() { + return function (input, check) { + return input === check; + }; + } + + function isNotIdenticalToFilter() { + return function (input, check) { + return input !== check; + }; + } +/** + * @ngdoc filter + * @name isNull + * @kind function + * + * @description + * checks if value is null or not + * @return Boolean + */ +angular.module('a8m.is-null', []) + .filter('isNull', function () { + return function(input) { + return isNull(input); + } + }); + +/** + * @ngdoc filter + * @name after-where + * @kind function + * + * @description + * get a collection and properties object, and returns all of the items + * in the collection after the first that found with the given properties. + * + */ +angular.module('a8m.after-where', []) + .filter('afterWhere', function() { + return function (collection, object) { + + collection = isObject(collection) + ? toArray(collection) + : collection; + + if(!isArray(collection) || isUndefined(object)) return collection; + + var index = collection.map( function( elm ) { + return objectContains(object, elm); + }).indexOf( true ); + + return collection.slice((index === -1) ? 0 : index); + } + }); + +/** + * @ngdoc filter + * @name after + * @kind function + * + * @description + * get a collection and specified count, and returns all of the items + * in the collection after the specified count. + * + */ + +angular.module('a8m.after', []) + .filter('after', function() { + return function (collection, count) { + collection = isObject(collection) + ? toArray(collection) + : collection; + + return (isArray(collection)) + ? collection.slice(count) + : collection; + } + }); + +/** + * @ngdoc filter + * @name before-where + * @kind function + * + * @description + * get a collection and properties object, and returns all of the items + * in the collection before the first that found with the given properties. + */ +angular.module('a8m.before-where', []) + .filter('beforeWhere', function() { + return function (collection, object) { + + collection = isObject(collection) + ? toArray(collection) + : collection; + + if(!isArray(collection) || isUndefined(object)) return collection; + + var index = collection.map( function( elm ) { + return objectContains(object, elm); + }).indexOf( true ); + + return collection.slice(0, (index === -1) ? collection.length : ++index); + } + }); + +/** + * @ngdoc filter + * @name before + * @kind function + * + * @description + * get a collection and specified count, and returns all of the items + * in the collection before the specified count. + */ +angular.module('a8m.before', []) + .filter('before', function() { + return function (collection, count) { + collection = isObject(collection) + ? toArray(collection) + : collection; + + return (isArray(collection)) + ? collection.slice(0, (!count) ? count : --count) + : collection; + } + }); + +/** + * @ngdoc filter + * @name chunkBy + * @kind function + * + * @description + * Collect data into fixed-length chunks or blocks + */ + +angular.module('a8m.chunk-by', ['a8m.filter-watcher']) + .filter('chunkBy', ['filterWatcher', function (filterWatcher) { + return function (array, n, fillVal) { + + return filterWatcher.isMemoized('chunkBy', arguments) || + filterWatcher.memoize('chunkBy', arguments, this, + _chunkBy(array, n, fillVal)); + /** + * @description + * Get array with size `n` in `val` inside it. + * @param n + * @param val + * @returns {Array} + */ + function fill(n, val) { + var ret = []; + while (n--) ret[n] = val; + return ret; + } + + function _chunkBy(array, n, fillVal) { + if (!isArray(array)) return array; + return array.map(function (el, i, self) { + i = i * n; + el = self.slice(i, i + n); + return !isUndefined(fillVal) && el.length < n + ? el.concat(fill(n - el.length, fillVal)) + : el; + }).slice(0, Math.ceil(array.length / n)); + } + } + }]); + +/** + * @ngdoc filter + * @name concat + * @kind function + * + * @description + * get (array/object, object/array) and return merged collection + */ +angular.module('a8m.concat', []) + .filter('concat', [function () { + return function (collection, joined) { + + if (isUndefined(joined)) return collection; + + if (isArray(collection)) { + return isObject(joined) + ? collection.concat(toArray(joined)) + : collection.concat(joined); + } + + if (isObject(collection)) { + var array = toArray(collection); + return (isObject(joined)) + ? array.concat(toArray(joined)) + : array.concat(joined); + } + return collection; + }; + } +]); + +/** + * @ngdoc filter + * @name contains + * @kind function + * + * @description + * Checks if given expression is present in one or more object in the collection + */ +angular.module('a8m.contains', []) + .filter({ + contains: ['$parse', containsFilter], + some: ['$parse', containsFilter] + }); + +function containsFilter($parse) { + return function (collection, expression) { + + collection = isObject(collection) ? toArray(collection) : collection; + + if(!isArray(collection) || isUndefined(expression)) { + return false; + } + + return collection.some(function(elm) { + return ((isString(expression) && isObject(elm)) || isFunction(expression)) + ? $parse(expression)(elm) + : elm === expression; + }); + + } + } + +/** + * @ngdoc filter + * @name countBy + * @kind function + * + * @description + * Sorts a list into groups and returns a count for the number of objects in each group. + */ + +angular.module('a8m.count-by', []) + + .filter('countBy', [ '$parse', function ( $parse ) { + return function (collection, property) { + + var result = {}, + get = $parse(property), + prop; + + collection = (isObject(collection)) ? toArray(collection) : collection; + + if(!isArray(collection) || isUndefined(property)) { + return collection; + } + + collection.forEach( function( elm ) { + prop = get(elm); + + if(!result[prop]) { + result[prop] = 0; + } + + result[prop]++; + }); + + return result; + } + }]); + +/** + * @ngdoc filter + * @name defaults + * @kind function + * + * @description + * defaultsFilter allows to specify a default fallback value for properties that resolve to undefined. + */ +angular.module('a8m.defaults', []) + .filter('defaults', ['$parse', function( $parse ) { + return function(collection, defaults) { + + collection = isObject(collection) ? toArray(collection) : collection; + + if(!isArray(collection) || !isObject(defaults)) { + return collection; + } + + var keys = deepKeys(defaults); + + collection.forEach(function(elm) { + //loop through all the keys + keys.forEach(function(key) { + var getter = $parse(key); + var setter = getter.assign; + //if it's not exist + if(isUndefined(getter(elm))) { + //get from defaults, and set to the returned object + setter(elm, getter(defaults)) + } + }); + }); + + return collection; + } + }]); +/** + * @ngdoc filter + * @name every + * @kind function + * + * @description + * Checks if given expression is present in all members in the collection + * + */ +angular.module('a8m.every', []) + .filter('every', ['$parse', function($parse) { + return function (collection, expression) { + collection = isObject(collection) ? toArray(collection) : collection; + + if(!isArray(collection) || isUndefined(expression)) { + return true; + } + + return collection.every( function(elm) { + return (isObject(elm) || isFunction(expression)) + ? $parse(expression)(elm) + : elm === expression; + }); + } + }]); + +/** + * @ngdoc filter + * @name filterBy + * @kind function + * + * @description + * filter by specific properties, avoid the rest + */ +angular.module('a8m.filter-by', []) + .filter('filterBy', ['$parse', function( $parse ) { + return function(collection, properties, search, strict) { + var comparator; + + search = (isString(search) || isNumber(search)) ? + String(search).toLowerCase() : undefined; + + collection = isObject(collection) ? toArray(collection) : collection; + + if(!isArray(collection) || isUndefined(search)) { + return collection; + } + + return collection.filter(function(elm) { + return properties.some(function(prop) { + + /** + * check if there is concatenate properties + * example: + * object: { first: 'foo', last:'bar' } + * filterBy: ['first + last'] => search by full name(i.e 'foo bar') + */ + if(!~prop.indexOf('+')) { + comparator = $parse(prop)(elm) + } else { + var propList = prop.replace(/\s+/g, '').split('+'); + comparator = propList + .map(function(prop) { return $parse(prop)(elm); }) + .join(' '); + } + + if (!isString(comparator) && !isNumber(comparator)) { + return false; + } + + comparator = String(comparator).toLowerCase(); + + return strict ? comparator === search : comparator.contains(search); + }); + }); + } + }]); + +/** + * @ngdoc filter + * @name first + * @kind function + * + * @description + * Gets the first element or first n elements of an array + * if callback is provided, is returns as long the callback return truthy + */ +angular.module('a8m.first', []) + .filter('first', ['$parse', function( $parse ) { + return function(collection) { + var n + , getter + , args; + + collection = isObject(collection) + ? toArray(collection) + : collection; + + if(!isArray(collection)) { + return collection; + } + + args = Array.prototype.slice.call(arguments, 1); + n = (isNumber(args[0])) ? args[0] : 1; + getter = (!isNumber(args[0])) ? args[0] : (!isNumber(args[1])) ? args[1] : undefined; + + return (args.length) ? getFirstMatches(collection, n,(getter) ? $parse(getter) : getter) : + collection[0]; + } + }]); + +/** + * @ngdoc filter + * @name flatten + * @kind function + * + * @description + * Flattens a nested array (the nesting can be to any depth). + * If you pass shallow, the array will only be flattened a single level + */ +angular.module('a8m.flatten', []) + .filter('flatten', function () { + return function(collection, shallow) { + + shallow = shallow || false; + collection = isObject(collection) + ? toArray(collection) + : collection; + + if(!isArray(collection)) { + return collection; + } + + return !shallow + ? flatten(collection, 0) + : [].concat.apply([], collection); + } + }); + +/** + * flatten nested array (the nesting can be to any depth). + * @param array {Array} + * @param i {int} + * @returns {Array} + * @private + */ +function flatten(array, i) { + i = i || 0; + + if(i >= array.length) + return array; + + if(isArray(array[i])) { + return flatten(array.slice(0,i) + .concat(array[i], array.slice(i+1)), i); + } + return flatten(array, i+1); +} + +/** + * @ngdoc filter + * @name fuzzyByKey + * @kind function + * + * @description + * fuzzy string searching by key + */ +angular.module('a8m.fuzzy-by', []) + .filter('fuzzyBy', ['$parse', function ( $parse ) { + return function (collection, property, search, csensitive) { + + var sensitive = csensitive || false, + prop, getter; + + collection = isObject(collection) ? toArray(collection) : collection; + + if(!isArray(collection) || isUndefined(property) + || isUndefined(search)) { + return collection; + } + + getter = $parse(property); + + return collection.filter(function(elm) { + + prop = getter(elm); + if(!isString(prop)) { + return false; + } + + prop = (sensitive) ? prop : prop.toLowerCase(); + search = (sensitive) ? search : search.toLowerCase(); + + return hasApproxPattern(prop, search) !== false + }) + } + + }]); +/** + * @ngdoc filter + * @name fuzzy + * @kind function + * + * @description + * fuzzy string searching for array of strings, objects + */ +angular.module('a8m.fuzzy', []) + .filter('fuzzy', function () { + return function (collection, search, csensitive) { + var sensitive = csensitive || false; + collection = isObject(collection) ? toArray(collection) : collection; + + if(!isArray(collection) || isUndefined(search)) { + return collection; + } + + search = (sensitive) ? search : search.toLowerCase(); + + return collection.filter(function(elm) { + if(isString(elm)) { + elm = (sensitive) ? elm : elm.toLowerCase(); + return hasApproxPattern(elm, search) !== false + } + return (isObject(elm)) ? _hasApproximateKey(elm, search) : false; + }); + + /** + * checks if object has key{string} that match + * to fuzzy search pattern + * @param object + * @param search + * @returns {boolean} + * @private + */ + function _hasApproximateKey(object, search) { + var properties = Object.keys(object), + prop, flag; + return 0 < properties.filter(function (elm) { + prop = object[elm]; + + //avoid iteration if we found some key that equal[performance] + if(flag) return true; + + if (isString(prop)) { + prop = (sensitive) ? prop : prop.toLowerCase(); + return flag = (hasApproxPattern(prop, search) !== false); + } + + return false; + + }).length; + } + } + }); + +/** + * @ngdoc filter + * @name groupBy + * @kind function + * + * @description + * Create an object composed of keys generated from the result of running each element of a collection, + * each key is an array of the elements. + */ + +angular.module('a8m.group-by', [ 'a8m.filter-watcher' ]) + .filter('groupBy', [ '$parse', 'filterWatcher', function ( $parse, filterWatcher ) { + return function (collection, property) { + + if(!isObject(collection) || isUndefined(property)) { + return collection; + } + + return filterWatcher.isMemoized('groupBy', arguments) || + filterWatcher.memoize('groupBy', arguments, this, + _groupBy(collection, $parse(property))); + + /** + * groupBy function + * @param collection + * @param getter + * @returns {{}} + */ + function _groupBy(collection, getter) { + var result = {}; + var prop; + + forEach( collection, function( elm ) { + prop = getter(elm); + + if(!result[prop]) { + result[prop] = []; + } + result[prop].push(elm); + }); + return result; + } + } + }]); + +/** + * @ngdoc filter + * @name isEmpty + * @kind function + * + * @description + * get collection or string and return if it empty + */ +angular.module('a8m.is-empty', []) + .filter('isEmpty', function () { + return function(collection) { + return isObject(collection) + ? !toArray(collection).length + : !collection.length; + } + }); + +/** + * @ngdoc filter + * @name join + * @kind function + * + * @description + * join a collection by a provided delimiter (space by default) + */ +angular.module('a8m.join', []) + .filter('join', function () { + return function (input, delimiter) { + if (isUndefined(input) || !isArray(input)) { + return input; + } + if (isUndefined(delimiter)) delimiter = ' '; + + return input.join(delimiter); + }; + }) +; + +/** + * @ngdoc filter + * @name last + * @kind function + * + * @description + * Gets the last element or last n elements of an array + * if callback is provided, is returns as long the callback return truthy + */ +angular.module('a8m.last', []) + .filter('last', ['$parse', function( $parse ) { + return function(collection) { + var n + , getter + , args + //cuz reverse change our src collection + //and we don't want side effects + , reversed = copy(collection); + + reversed = isObject(reversed) + ? toArray(reversed) + : reversed; + + if(!isArray(reversed)) { + return reversed; + } + + args = Array.prototype.slice.call(arguments, 1); + n = (isNumber(args[0])) ? args[0] : 1; + getter = (!isNumber(args[0])) ? args[0] : (!isNumber(args[1])) ? args[1] : undefined; + + return (args.length) + //send reversed collection as arguments, and reverse it back as result + ? getFirstMatches(reversed.reverse(), n,(getter) ? $parse(getter) : getter).reverse() + //get the last element + : reversed[reversed.length-1]; + } + }]); + +/** + * @ngdoc filter + * @name map + * @kind function + * + * @description + * Returns a new collection of the results of each expression execution. + */ +angular.module('a8m.map', []) + .filter('map', ['$parse', function($parse) { + return function (collection, expression) { + + collection = isObject(collection) + ? toArray(collection) + : collection; + + if(!isArray(collection) || isUndefined(expression)) { + return collection; + } + + return collection.map(function (elm) { + return $parse(expression)(elm); + }); + } + }]); + +/** + * @ngdoc filter + * @name omit + * @kind function + * + * @description + * filter collection by expression + */ + +angular.module('a8m.omit', []) + + .filter('omit', ['$parse', function($parse) { + return function (collection, expression) { + + collection = isObject(collection) + ? toArray(collection) + : collection; + + if(!isArray(collection) || isUndefined(expression)) { + return collection; + } + + return collection.filter(function (elm) { + return !($parse(expression)(elm)); + }); + } + }]); + +/** + * @ngdoc filter + * @name pick + * @kind function + * + * @description + * filter collection by expression + */ + +angular.module('a8m.pick', []) + + .filter('pick', ['$parse', function($parse) { + return function (collection, expression) { + + collection = isObject(collection) + ? toArray(collection) + : collection; + + if(!isArray(collection) || isUndefined(expression)) { + return collection; + } + + return collection.filter(function (elm) { + return $parse(expression)(elm); + }); + } + }]); + +/** + * @ngdoc filter + * @name range + * @kind function + * + * @description + * rangeFilter provides some support for a for loop using numbers + */ +angular.module('a8m.range', []) + .filter('range', function () { + return function (input, total, start, increment, cb) { + start = start || 0; + increment = increment || 1; + for (var i = 0; i < parseInt(total); i++) { + var j = start + i * increment; + input.push(isFunction(cb) ? cb(j) : j); + } + return input; + }; + }); +/** + * @ngdoc filter + * @name removeWith + * @kind function + * + * @description + * get collection and properties object, and removed elements + * with this properties + */ + +angular.module('a8m.remove-with', []) + .filter('removeWith', function() { + return function (collection, object) { + + if(isUndefined(object)) { + return collection; + } + collection = isObject(collection) + ? toArray(collection) + : collection; + + return collection.filter(function (elm) { + return !objectContains(object, elm); + }); + } + }); + + +/** + * @ngdoc filter + * @name remove + * @kind function + * + * @description + * remove specific members from collection + */ + +angular.module('a8m.remove', []) + + .filter('remove', function () { + return function (collection) { + collection = isObject(collection) ? toArray(collection) : collection; + var args = Array.prototype.slice.call(arguments, 1); + + if(!isArray(collection)) { + return collection; + } + + return collection.filter( function( member ) { + return !args.some(function(nest) { + return equals(nest, member); + }) + }); + } + }); + +/** + * @ngdoc filter + * @name reverse + * @kind function + * + * @description + * Reverses a string or collection + */ +angular.module('a8m.reverse', []) + .filter('reverse',[ function () { + return function (input) { + input = isObject(input) ? toArray(input) : input; + + if(isString(input)) { + return input.split('').reverse().join(''); + } + + return isArray(input) + ? input.slice().reverse() + : input; + } + }]); + +/** + * @ngdoc filter + * @name searchField + * @kind function + * + * @description + * for each member, join several strings field and add them to + * new field called 'searchField' (use for search filtering) + */ +angular.module('a8m.search-field', []) + .filter('searchField', ['$parse', function ($parse) { + return function (collection) { + + var get, field; + + collection = isObject(collection) ? toArray(collection) : collection; + + var args = Array.prototype.slice.call(arguments, 1); + + if(!isArray(collection) || !args.length) { + return collection; + } + + return collection.map(function(member) { + + field = args.map(function(field) { + get = $parse(field); + return get(member); + }).join(' '); + + return extend(member, { searchField: field }); + }); + } + }]); + +/** + * @ngdoc filter + * @name toArray + * @kind function + * + * @description + * Convert objects into stable arrays. + * if addKey set to true,the filter also attaches a new property + * $key to the value containing the original key that was used in + * the object we are iterating over to reference the property + */ +angular.module('a8m.to-array', []) + .filter('toArray', function() { + return function (collection, addKey) { + + if(!isObject(collection)) { + return collection; + } + + return !addKey + ? toArray(collection) + : Object.keys(collection).map(function (key) { + return extend(collection[key], { $key: key }); + }); + } + }); + +/** + * @ngdoc filter + * @name unique/uniq + * @kind function + * + * @description + * get collection and filter duplicate members + * if uniqueFilter get a property(nested to) as argument it's + * filter by this property as unique identifier + */ + +angular.module('a8m.unique', []) + .filter({ + unique: ['$parse', uniqFilter], + uniq: ['$parse', uniqFilter] + }); + +function uniqFilter($parse) { + return function (collection, property) { + + collection = isObject(collection) ? toArray(collection) : collection; + + if (!isArray(collection)) { + return collection; + } + + //store all unique identifiers + var uniqueItems = [], + get = $parse(property); + + return (isUndefined(property)) + //if it's kind of primitive array + ? collection.filter(function (elm, pos, self) { + return self.indexOf(elm) === pos; + }) + //else compare with equals + : collection.filter(function (elm) { + var prop = get(elm); + if(some(uniqueItems, prop)) { + return false; + } + uniqueItems.push(prop); + return true; + }); + + //checked if the unique identifier is already exist + function some(array, member) { + if(isUndefined(member)) { + return false; + } + return array.some(function(el) { + return equals(el, member); + }); + } + } +} + +/** + * @ngdoc filter + * @name where + * @kind function + * + * @description + * of each element in a collection to the given properties object, + * returning an array of all elements that have equivalent property values. + * + */ +angular.module('a8m.where', []) + .filter('where', function() { + return function (collection, object) { + if(isUndefined(object)) return collection; + collection = isObject(collection) + ? toArray(collection) + : collection; + + return collection.filter(function (elm) { + return objectContains(object, elm); + }); + } + }); + +/** + * @ngdoc filter + * @name xor + * @kind function + * + * @description + * Exclusive or filter by expression + */ + +angular.module('a8m.xor', []) + + .filter('xor', ['$parse', function($parse) { + return function (col1, col2, expression) { + + expression = expression || false; + + col1 = isObject(col1) ? toArray(col1) : col1; + col2 = isObject(col2) ? toArray(col2) : col2; + + if(!isArray(col1) || !isArray(col2)) return col1; + + return col1.concat(col2) + .filter(function(elm) { + return !(some(elm, col1) && some(elm, col2)); + }); + + function some(el, col) { + var getter = $parse(expression); + return col.some(function(dElm) { + return expression + ? equals(getter(dElm), getter(el)) + : equals(dElm, el); + }); + } + } + }]); + +/** + * @ngdoc filter + * @name abs + * @kind function + * + * @description + * Will return the absolute value of a number + */ +angular.module('a8m.math.abs', []) + .filter('abs', function () { + return function (input) { + return Math.abs(input); + } + }); + +/** + * @ngdoc filter + * @name formatBytes + * @kind function + * + * @description + * Convert bytes into appropriate display + * 1024 bytes => 1 KB + */ +angular.module('a8m.math.byteFmt', []) + .filter('byteFmt', function () { + var compared = [{str: 'B', val: 1024}]; + ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'].forEach(function(el, i) { + compared.push({str: el, val: compared[i].val * 1024 }); + }); + return function (bytes, decimal) { + if(isNumber(decimal) && isFinite(decimal) && decimal%1===0 && decimal >= 0 && + isNumber(bytes) && isFinite(bytes)) { + var i = 0; + while (i < compared.length-1 && bytes >= compared[i].val) i++; + bytes /= i > 0 ? compared[i-1].val : 1; + return convertToDecimal(bytes, decimal) + ' ' + compared[i].str; + } + return 'NaN'; + } + }); + +/** + * @ngdoc filter + * @name degrees + * @kind function + * + * @description + * Convert angle from radians to degrees + */ +angular.module('a8m.math.degrees', []) + .filter('degrees', function () { + return function (radians, decimal) { + // if decimal is not an integer greater than -1, we cannot do. quit with error "NaN" + // if degrees is not a real number, we cannot do also. quit with error "NaN" + if(isNumber(decimal) && isFinite(decimal) && decimal%1===0 && decimal >= 0 && + isNumber(radians) && isFinite(radians)) { + var degrees = (radians * 180) / Math.PI; + return Math.round(degrees * Math.pow(10,decimal)) / (Math.pow(10,decimal)); + } else { + return 'NaN'; + } + } + }); + + + +/** + * @ngdoc filter + * @name formatBytes + * @kind function + * + * @description + * Convert bytes into appropriate display + * 1024 kilobytes => 1 MB + */ +angular.module('a8m.math.kbFmt', []) + .filter('kbFmt', function () { + var compared = [{str: 'KB', val: 1024}]; + ['MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'].forEach(function(el, i) { + compared.push({str: el, val: compared[i].val * 1024 }); + }); + return function (bytes, decimal) { + if(isNumber(decimal) && isFinite(decimal) && decimal%1===0 && decimal >= 0 && + isNumber(bytes) && isFinite(bytes)) { + var i = 0; + while (i < compared.length-1 && bytes >= compared[i].val) i++; + bytes /= i > 0 ? compared[i-1].val : 1; + return convertToDecimal(bytes, decimal) + ' ' + compared[i].str; + } + return 'NaN'; + } + }); +/** + * @ngdoc filter + * @name max + * @kind function + * + * @description + * Math.max will get an array and return the max value. if an expression + * is provided, will return max value by expression. + */ +angular.module('a8m.math.max', []) + .filter('max', ['$parse', function ($parse) { + return function (input, expression) { + + if(!isArray(input)) { + return input; + } + return isUndefined(expression) + ? Math.max.apply(Math, input) + : input[indexByMax(input, expression)]; + }; + + /** + * @private + * @param array + * @param exp + * @returns {number|*|Number} + */ + function indexByMax(array, exp) { + var mappedArray = array.map(function(elm){ + return $parse(exp)(elm); + }); + return mappedArray.indexOf(Math.max.apply(Math, mappedArray)); + } + }]); +/** + * @ngdoc filter + * @name min + * @kind function + * + * @description + * Math.min will get an array and return the min value. if an expression + * is provided, will return min value by expression. + */ +angular.module('a8m.math.min', []) + .filter('min', ['$parse', function ($parse) { + return function (input, expression) { + + if(!isArray(input)) { + return input; + } + return isUndefined(expression) + ? Math.min.apply(Math, input) + : input[indexByMin(input, expression)]; + }; + + /** + * @private + * @param array + * @param exp + * @returns {number|*|Number} + */ + function indexByMin(array, exp) { + var mappedArray = array.map(function(elm){ + return $parse(exp)(elm); + }); + return mappedArray.indexOf(Math.min.apply(Math, mappedArray)); + } + }]); +/** + * @ngdoc filter + * @name Percent + * @kind function + * + * @description + * percentage between two numbers + */ +angular.module('a8m.math.percent', []) + .filter('percent', function () { + return function (input, divided, round) { + + var divider = isString(input) ? Number(input) : input; + divided = divided || 100; + round = round || false; + + if (!isNumber(divider) || isNaN(divider)) return input; + + return round + ? Math.round((divider / divided) * 100) + : (divider / divided) * 100; + } + }); + +/** + * @ngdoc filter + * @name toRadians + * @kind function + * + * @description + * Convert angle from degrees to radians + */ +angular.module('a8m.math.radians', []) + .filter('radians', function() { + return function (degrees, decimal) { + // if decimal is not an integer greater than -1, we cannot do. quit with error "NaN" + // if degrees is not a real number, we cannot do also. quit with error "NaN" + if(isNumber(decimal) && isFinite(decimal) && decimal%1===0 && decimal >= 0 && + isNumber(degrees) && isFinite(degrees)) { + var radians = (degrees * 3.14159265359) / 180; + return Math.round(radians * Math.pow(10,decimal)) / (Math.pow(10,decimal)); + } + return 'NaN'; + } + }); + + + +/** + * @ngdoc filter + * @name Radix + * @kind function + * + * @description + * converting decimal numbers to different bases(radix) + */ +angular.module('a8m.math.radix', []) + .filter('radix', function () { + return function (input, radix) { + var RANGE = /^[2-9]$|^[1-2]\d$|^3[0-6]$/; + + if(!isNumber(input) || !RANGE.test(radix)) { + return input; + } + + return input.toString(radix).toUpperCase(); + } + }); + +/** + * @ngdoc filter + * @name formatBytes + * @kind function + * + * @description + * Convert number into abbreviations. + * i.e: K for one thousand, M for Million, B for billion + * e.g: number of users:235,221, decimal:1 => 235.2 K + */ +angular.module('a8m.math.shortFmt', []) + .filter('shortFmt', function () { + return function (number, decimal) { + if(isNumber(decimal) && isFinite(decimal) && decimal%1===0 && decimal >= 0 && + isNumber(number) && isFinite(number)){ + if(number < 1e3) { + return '' + number; // Coerce to string + } else if(number < 1e6) { + return convertToDecimal((number / 1e3), decimal) + ' K'; + } else if(number < 1e9){ + return convertToDecimal((number / 1e6), decimal) + ' M'; + } else { + return convertToDecimal((number / 1e9), decimal) + ' B'; + } + + } + return 'NaN'; + } + }); +/** + * @ngdoc filter + * @name sum + * @kind function + * + * @description + * Sum up all values within an array + */ +angular.module('a8m.math.sum', []) + .filter('sum', function () { + return function (input, initial) { + return !isArray(input) + ? input + : input.reduce(function(prev, curr) { + return prev + curr; + }, initial || 0); + } + }); + +/** + * @ngdoc filter + * @name endsWith + * @kind function + * + * @description + * checks whether string ends with the ends parameter. + */ +angular.module('a8m.ends-with', []) + + .filter('endsWith', function () { + return function (input, ends, csensitive) { + + var sensitive = csensitive || false, + position; + + if(!isString(input) || isUndefined(ends)) { + return input; + } + + input = (sensitive) ? input : input.toLowerCase(); + position = input.length - ends.length; + + return input.indexOf((sensitive) ? ends : ends.toLowerCase(), position) !== -1; + } + }); + +/** + * @ngdoc filter + * @name latinize + * @kind function + * + * @description + * remove accents/diacritics from a string + */ +angular.module('a8m.latinize', []) + .filter('latinize',[ function () { + var defaultDiacriticsRemovalap = [ + {'base':'A', 'letters':'\u0041\u24B6\uFF21\u00C0\u00C1\u00C2\u1EA6\u1EA4\u1EAA\u1EA8\u00C3\u0100\u0102\u1EB0\u1EAE\u1EB4\u1EB2\u0226\u01E0\u00C4\u01DE\u1EA2\u00C5\u01FA\u01CD\u0200\u0202\u1EA0\u1EAC\u1EB6\u1E00\u0104\u023A\u2C6F'}, + {'base':'AA','letters':'\uA732'}, + {'base':'AE','letters':'\u00C6\u01FC\u01E2'}, + {'base':'AO','letters':'\uA734'}, + {'base':'AU','letters':'\uA736'}, + {'base':'AV','letters':'\uA738\uA73A'}, + {'base':'AY','letters':'\uA73C'}, + {'base':'B', 'letters':'\u0042\u24B7\uFF22\u1E02\u1E04\u1E06\u0243\u0182\u0181'}, + {'base':'C', 'letters':'\u0043\u24B8\uFF23\u0106\u0108\u010A\u010C\u00C7\u1E08\u0187\u023B\uA73E'}, + {'base':'D', 'letters':'\u0044\u24B9\uFF24\u1E0A\u010E\u1E0C\u1E10\u1E12\u1E0E\u0110\u018B\u018A\u0189\uA779'}, + {'base':'DZ','letters':'\u01F1\u01C4'}, + {'base':'Dz','letters':'\u01F2\u01C5'}, + {'base':'E', 'letters':'\u0045\u24BA\uFF25\u00C8\u00C9\u00CA\u1EC0\u1EBE\u1EC4\u1EC2\u1EBC\u0112\u1E14\u1E16\u0114\u0116\u00CB\u1EBA\u011A\u0204\u0206\u1EB8\u1EC6\u0228\u1E1C\u0118\u1E18\u1E1A\u0190\u018E'}, + {'base':'F', 'letters':'\u0046\u24BB\uFF26\u1E1E\u0191\uA77B'}, + {'base':'G', 'letters':'\u0047\u24BC\uFF27\u01F4\u011C\u1E20\u011E\u0120\u01E6\u0122\u01E4\u0193\uA7A0\uA77D\uA77E'}, + {'base':'H', 'letters':'\u0048\u24BD\uFF28\u0124\u1E22\u1E26\u021E\u1E24\u1E28\u1E2A\u0126\u2C67\u2C75\uA78D'}, + {'base':'I', 'letters':'\u0049\u24BE\uFF29\u00CC\u00CD\u00CE\u0128\u012A\u012C\u0130\u00CF\u1E2E\u1EC8\u01CF\u0208\u020A\u1ECA\u012E\u1E2C\u0197'}, + {'base':'J', 'letters':'\u004A\u24BF\uFF2A\u0134\u0248'}, + {'base':'K', 'letters':'\u004B\u24C0\uFF2B\u1E30\u01E8\u1E32\u0136\u1E34\u0198\u2C69\uA740\uA742\uA744\uA7A2'}, + {'base':'L', 'letters':'\u004C\u24C1\uFF2C\u013F\u0139\u013D\u1E36\u1E38\u013B\u1E3C\u1E3A\u0141\u023D\u2C62\u2C60\uA748\uA746\uA780'}, + {'base':'LJ','letters':'\u01C7'}, + {'base':'Lj','letters':'\u01C8'}, + {'base':'M', 'letters':'\u004D\u24C2\uFF2D\u1E3E\u1E40\u1E42\u2C6E\u019C'}, + {'base':'N', 'letters':'\u004E\u24C3\uFF2E\u01F8\u0143\u00D1\u1E44\u0147\u1E46\u0145\u1E4A\u1E48\u0220\u019D\uA790\uA7A4'}, + {'base':'NJ','letters':'\u01CA'}, + {'base':'Nj','letters':'\u01CB'}, + {'base':'O', 'letters':'\u004F\u24C4\uFF2F\u00D2\u00D3\u00D4\u1ED2\u1ED0\u1ED6\u1ED4\u00D5\u1E4C\u022C\u1E4E\u014C\u1E50\u1E52\u014E\u022E\u0230\u00D6\u022A\u1ECE\u0150\u01D1\u020C\u020E\u01A0\u1EDC\u1EDA\u1EE0\u1EDE\u1EE2\u1ECC\u1ED8\u01EA\u01EC\u00D8\u01FE\u0186\u019F\uA74A\uA74C'}, + {'base':'OI','letters':'\u01A2'}, + {'base':'OO','letters':'\uA74E'}, + {'base':'OU','letters':'\u0222'}, + {'base':'OE','letters':'\u008C\u0152'}, + {'base':'oe','letters':'\u009C\u0153'}, + {'base':'P', 'letters':'\u0050\u24C5\uFF30\u1E54\u1E56\u01A4\u2C63\uA750\uA752\uA754'}, + {'base':'Q', 'letters':'\u0051\u24C6\uFF31\uA756\uA758\u024A'}, + {'base':'R', 'letters':'\u0052\u24C7\uFF32\u0154\u1E58\u0158\u0210\u0212\u1E5A\u1E5C\u0156\u1E5E\u024C\u2C64\uA75A\uA7A6\uA782'}, + {'base':'S', 'letters':'\u0053\u24C8\uFF33\u1E9E\u015A\u1E64\u015C\u1E60\u0160\u1E66\u1E62\u1E68\u0218\u015E\u2C7E\uA7A8\uA784'}, + {'base':'T', 'letters':'\u0054\u24C9\uFF34\u1E6A\u0164\u1E6C\u021A\u0162\u1E70\u1E6E\u0166\u01AC\u01AE\u023E\uA786'}, + {'base':'TZ','letters':'\uA728'}, + {'base':'U', 'letters':'\u0055\u24CA\uFF35\u00D9\u00DA\u00DB\u0168\u1E78\u016A\u1E7A\u016C\u00DC\u01DB\u01D7\u01D5\u01D9\u1EE6\u016E\u0170\u01D3\u0214\u0216\u01AF\u1EEA\u1EE8\u1EEE\u1EEC\u1EF0\u1EE4\u1E72\u0172\u1E76\u1E74\u0244'}, + {'base':'V', 'letters':'\u0056\u24CB\uFF36\u1E7C\u1E7E\u01B2\uA75E\u0245'}, + {'base':'VY','letters':'\uA760'}, + {'base':'W', 'letters':'\u0057\u24CC\uFF37\u1E80\u1E82\u0174\u1E86\u1E84\u1E88\u2C72'}, + {'base':'X', 'letters':'\u0058\u24CD\uFF38\u1E8A\u1E8C'}, + {'base':'Y', 'letters':'\u0059\u24CE\uFF39\u1EF2\u00DD\u0176\u1EF8\u0232\u1E8E\u0178\u1EF6\u1EF4\u01B3\u024E\u1EFE'}, + {'base':'Z', 'letters':'\u005A\u24CF\uFF3A\u0179\u1E90\u017B\u017D\u1E92\u1E94\u01B5\u0224\u2C7F\u2C6B\uA762'}, + {'base':'a', 'letters':'\u0061\u24D0\uFF41\u1E9A\u00E0\u00E1\u00E2\u1EA7\u1EA5\u1EAB\u1EA9\u00E3\u0101\u0103\u1EB1\u1EAF\u1EB5\u1EB3\u0227\u01E1\u00E4\u01DF\u1EA3\u00E5\u01FB\u01CE\u0201\u0203\u1EA1\u1EAD\u1EB7\u1E01\u0105\u2C65\u0250'}, + {'base':'aa','letters':'\uA733'}, + {'base':'ae','letters':'\u00E6\u01FD\u01E3'}, + {'base':'ao','letters':'\uA735'}, + {'base':'au','letters':'\uA737'}, + {'base':'av','letters':'\uA739\uA73B'}, + {'base':'ay','letters':'\uA73D'}, + {'base':'b', 'letters':'\u0062\u24D1\uFF42\u1E03\u1E05\u1E07\u0180\u0183\u0253'}, + {'base':'c', 'letters':'\u0063\u24D2\uFF43\u0107\u0109\u010B\u010D\u00E7\u1E09\u0188\u023C\uA73F\u2184'}, + {'base':'d', 'letters':'\u0064\u24D3\uFF44\u1E0B\u010F\u1E0D\u1E11\u1E13\u1E0F\u0111\u018C\u0256\u0257\uA77A'}, + {'base':'dz','letters':'\u01F3\u01C6'}, + {'base':'e', 'letters':'\u0065\u24D4\uFF45\u00E8\u00E9\u00EA\u1EC1\u1EBF\u1EC5\u1EC3\u1EBD\u0113\u1E15\u1E17\u0115\u0117\u00EB\u1EBB\u011B\u0205\u0207\u1EB9\u1EC7\u0229\u1E1D\u0119\u1E19\u1E1B\u0247\u025B\u01DD'}, + {'base':'f', 'letters':'\u0066\u24D5\uFF46\u1E1F\u0192\uA77C'}, + {'base':'g', 'letters':'\u0067\u24D6\uFF47\u01F5\u011D\u1E21\u011F\u0121\u01E7\u0123\u01E5\u0260\uA7A1\u1D79\uA77F'}, + {'base':'h', 'letters':'\u0068\u24D7\uFF48\u0125\u1E23\u1E27\u021F\u1E25\u1E29\u1E2B\u1E96\u0127\u2C68\u2C76\u0265'}, + {'base':'hv','letters':'\u0195'}, + {'base':'i', 'letters':'\u0069\u24D8\uFF49\u00EC\u00ED\u00EE\u0129\u012B\u012D\u00EF\u1E2F\u1EC9\u01D0\u0209\u020B\u1ECB\u012F\u1E2D\u0268\u0131'}, + {'base':'j', 'letters':'\u006A\u24D9\uFF4A\u0135\u01F0\u0249'}, + {'base':'k', 'letters':'\u006B\u24DA\uFF4B\u1E31\u01E9\u1E33\u0137\u1E35\u0199\u2C6A\uA741\uA743\uA745\uA7A3'}, + {'base':'l', 'letters':'\u006C\u24DB\uFF4C\u0140\u013A\u013E\u1E37\u1E39\u013C\u1E3D\u1E3B\u017F\u0142\u019A\u026B\u2C61\uA749\uA781\uA747'}, + {'base':'lj','letters':'\u01C9'}, + {'base':'m', 'letters':'\u006D\u24DC\uFF4D\u1E3F\u1E41\u1E43\u0271\u026F'}, + {'base':'n', 'letters':'\u006E\u24DD\uFF4E\u01F9\u0144\u00F1\u1E45\u0148\u1E47\u0146\u1E4B\u1E49\u019E\u0272\u0149\uA791\uA7A5'}, + {'base':'nj','letters':'\u01CC'}, + {'base':'o', 'letters':'\u006F\u24DE\uFF4F\u00F2\u00F3\u00F4\u1ED3\u1ED1\u1ED7\u1ED5\u00F5\u1E4D\u022D\u1E4F\u014D\u1E51\u1E53\u014F\u022F\u0231\u00F6\u022B\u1ECF\u0151\u01D2\u020D\u020F\u01A1\u1EDD\u1EDB\u1EE1\u1EDF\u1EE3\u1ECD\u1ED9\u01EB\u01ED\u00F8\u01FF\u0254\uA74B\uA74D\u0275'}, + {'base':'oi','letters':'\u01A3'}, + {'base':'ou','letters':'\u0223'}, + {'base':'oo','letters':'\uA74F'}, + {'base':'p','letters':'\u0070\u24DF\uFF50\u1E55\u1E57\u01A5\u1D7D\uA751\uA753\uA755'}, + {'base':'q','letters':'\u0071\u24E0\uFF51\u024B\uA757\uA759'}, + {'base':'r','letters':'\u0072\u24E1\uFF52\u0155\u1E59\u0159\u0211\u0213\u1E5B\u1E5D\u0157\u1E5F\u024D\u027D\uA75B\uA7A7\uA783'}, + {'base':'s','letters':'\u0073\u24E2\uFF53\u00DF\u015B\u1E65\u015D\u1E61\u0161\u1E67\u1E63\u1E69\u0219\u015F\u023F\uA7A9\uA785\u1E9B'}, + {'base':'t','letters':'\u0074\u24E3\uFF54\u1E6B\u1E97\u0165\u1E6D\u021B\u0163\u1E71\u1E6F\u0167\u01AD\u0288\u2C66\uA787'}, + {'base':'tz','letters':'\uA729'}, + {'base':'u','letters': '\u0075\u24E4\uFF55\u00F9\u00FA\u00FB\u0169\u1E79\u016B\u1E7B\u016D\u00FC\u01DC\u01D8\u01D6\u01DA\u1EE7\u016F\u0171\u01D4\u0215\u0217\u01B0\u1EEB\u1EE9\u1EEF\u1EED\u1EF1\u1EE5\u1E73\u0173\u1E77\u1E75\u0289'}, + {'base':'v','letters':'\u0076\u24E5\uFF56\u1E7D\u1E7F\u028B\uA75F\u028C'}, + {'base':'vy','letters':'\uA761'}, + {'base':'w','letters':'\u0077\u24E6\uFF57\u1E81\u1E83\u0175\u1E87\u1E85\u1E98\u1E89\u2C73'}, + {'base':'x','letters':'\u0078\u24E7\uFF58\u1E8B\u1E8D'}, + {'base':'y','letters':'\u0079\u24E8\uFF59\u1EF3\u00FD\u0177\u1EF9\u0233\u1E8F\u00FF\u1EF7\u1E99\u1EF5\u01B4\u024F\u1EFF'}, + {'base':'z','letters':'\u007A\u24E9\uFF5A\u017A\u1E91\u017C\u017E\u1E93\u1E95\u01B6\u0225\u0240\u2C6C\uA763'} + ]; + + var diacriticsMap = {}; + for (var i = 0; i < defaultDiacriticsRemovalap.length; i++) { + var letters = defaultDiacriticsRemovalap[i].letters.split(""); + for (var j = 0; j < letters.length ; j++){ + diacriticsMap[letters[j]] = defaultDiacriticsRemovalap[i].base; + } + } + + // "what?" version ... http://jsperf.com/diacritics/12 + function removeDiacritics (str) { + return str.replace(/[^\u0000-\u007E]/g, function(a){ + return diacriticsMap[a] || a; + }); + } + + return function (input) { + + return isString(input) + ? removeDiacritics(input) + : input; + } + }]); + +/** + * @ngdoc filter + * @name ltrim + * @kind function + * + * @description + * Left trim. Similar to trimFilter, but only for left side. + */ +angular.module('a8m.ltrim', []) + .filter('ltrim', function () { + return function(input, chars) { + + var trim = chars || '\\s'; + + return isString(input) + ? input.replace(new RegExp('^' + trim + '+'), '') + : input; + } + }); + +/** + * @ngdoc filter + * @name match + * @kind function + * + * @description + * Return the matched pattern in a string. + */ +angular.module('a8m.match', []) + .filter('match', function () { + return function (input, pattern, flag) { + + var reg = new RegExp(pattern, flag); + + return isString(input) + ? input.match(reg) + : null; + } + }); + +/** + * @ngdoc filter + * @name phone-us + * @kind function + * + * @description + * format a string or a number into a us-style + * phone number in the form (***) ***-**** + */ +angular.module('a8m.phoneUS', []) + .filter('phoneUS', function () { + return function(num) { + num += ''; + return '(' + num.slice(0, 3) + ') ' + num.slice(3, 6) + '-' + num.slice(6); + } + }); + +/** + * @ngdoc filter + * @name repeat + * @kind function + * + * @description + * Repeats a string n times + */ +angular.module('a8m.repeat', []) + .filter('repeat',[ function () { + return function (input, n, separator) { + + var times = ~~n; + + if(!isString(input)) { + return input; + } + + return !times + ? input + : strRepeat(input, --n, separator || ''); + } + }]); + +/** + * Repeats a string n times with given separator + * @param str string to repeat + * @param n number of times + * @param sep separator + * @returns {*} + */ +function strRepeat(str, n, sep) { + if(!n) { + return str; + } + return str + sep + strRepeat(str, --n, sep); +} +/** +* @ngdoc filter +* @name rtrim +* @kind function +* +* @description +* Right trim. Similar to trimFilter, but only for right side. +*/ +angular.module('a8m.rtrim', []) + .filter('rtrim', function () { + return function(input, chars) { + + var trim = chars || '\\s'; + + return isString(input) + ? input.replace(new RegExp(trim + '+$'), '') + : input; + } + }); + +/** + * @ngdoc filter + * @name slugify + * @kind function + * + * @description + * remove spaces from string, replace with "-" or given argument + */ +angular.module('a8m.slugify', []) + .filter('slugify',[ function () { + return function (input, sub) { + + var replace = (isUndefined(sub)) ? '-' : sub; + + return isString(input) + ? input.toLowerCase().replace(/\s+/g, replace) + : input; + } + }]); + +/** + * @ngdoc filter + * @name split + * @kind function + * + * @description + * split a string by a provided delimiter (none '' by default) and skip first n-delimiters + */ +angular.module('a8m.split', []) + .filter('split', function () { + function escapeRegExp(str) { + return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); + } + + return function (input, delimiter, skip) { + var _regexp, _matches, _splitted, _temp; + + if (isUndefined(input) || !isString(input)) { + return null; + } + if (isUndefined(delimiter)) delimiter = ''; + if (isNaN(skip)) skip = 0; + + _regexp = new RegExp(escapeRegExp(delimiter), 'g'); + _matches = input.match(_regexp); + + if (isNull(_matches) || skip >= _matches.length) { + return [input]; + } + + if (skip === 0) return input.split(delimiter); + + _splitted = input.split(delimiter); + _temp = _splitted.splice(0, skip + 1); + _splitted.unshift(_temp.join(delimiter)); + + return _splitted; + }; + }) +; + +/** + * @ngdoc filter + * @name startWith + * @kind function + * + * @description + * checks whether string starts with the starts parameter. + */ +angular.module('a8m.starts-with', []) + .filter('startsWith', function () { + return function (input, start, csensitive) { + + var sensitive = csensitive || false; + + if(!isString(input) || isUndefined(start)) { + return input; + } + + input = (sensitive) ? input : input.toLowerCase(); + + return !input.indexOf((sensitive) ? start : start.toLowerCase()); + } + }); + +/** + * @ngdoc filter + * @name stringular + * @kind function + * + * @description + * get string with {n} and replace match with enumeration values + */ +angular.module('a8m.stringular', []) + .filter('stringular', function () { + return function(input) { + + var args = Array.prototype.slice.call(arguments, 1); + + return input.replace(/{(\d+)}/g, function (match, number) { + return isUndefined(args[number]) ? match : args[number]; + }); + } + }); + +/** + * @ngdoc filter + * @name stripTags + * @kind function + * + * @description + * strip html tags from string + */ +angular.module('a8m.strip-tags', []) + .filter('stripTags', function () { + return function(input) { + return isString(input) + ? input.replace(/<\S[^><]*>/g, '') + : input; + } + }); + +/** + * @ngdoc filter + * @name test + * @kind function + * + * @description + * test if a string match a pattern. + */ +angular.module('a8m.test', []) + .filter('test', function () { + return function (input, pattern, flag) { + + var reg = new RegExp(pattern, flag); + + return isString(input) + ? reg.test(input) + : input; + } + }); + +/** + * @ngdoc filter + * @name trim + * @kind function + * + * @description + * Strip whitespace (or other characters) from the beginning and end of a string + */ +angular.module('a8m.trim', []) + .filter('trim', function () { + return function(input, chars) { + + var trim = chars || '\\s'; + + return isString(input) + ? input.replace(new RegExp('^' + trim + '+|' + trim + '+$', 'g'), '') + : input; + } + }); + +/** + * @ngdoc filter + * @name truncate + * @kind function + * + * @description + * truncates a string given a specified length, providing a custom string to denote an omission. + */ +angular.module('a8m.truncate', []) + .filter('truncate', function () { + return function(input, length, suffix, preserve) { + + length = isUndefined(length) ? input.length : length; + preserve = preserve || false; + suffix = suffix || ''; + + if(!isString(input) || (input.length <= length)) return input; + + return input.substring(0, (preserve) + ? ((input.indexOf(' ', length) === -1) ? input.length : input.indexOf(' ', length)) + : length) + suffix; + }; + }); + +/** + * @ngdoc filter + * @name ucfirst + * @kind function + * + * @description + * ucfirst + */ +angular.module('a8m.ucfirst', []) + .filter({ + ucfirst: ucfirstFilter, + titleize: ucfirstFilter + }); + +function ucfirstFilter() { + return function (input) { + return isString(input) + ? input + .split(' ') + .map(function (ch) { + return ch.charAt(0).toUpperCase() + ch.substring(1); + }) + .join(' ') + : input; + } +} + +/** + * @ngdoc filter + * @name uriComponentEncode + * @kind function + * + * @description + * get string as parameter and return encoded string + */ +angular.module('a8m.uri-component-encode', []) + .filter('uriComponentEncode',['$window', function ($window) { + return function (input) { + return isString(input) + ? $window.encodeURIComponent(input) + : input; + } + }]); + +/** + * @ngdoc filter + * @name uriEncode + * @kind function + * + * @description + * get string as parameter and return encoded string + */ +angular.module('a8m.uri-encode', []) + .filter('uriEncode',['$window', function ($window) { + return function (input) { + return isString(input) + ? $window.encodeURI(input) + : input; + } + }]); + +/** + * @ngdoc filter + * @name wrap + * @kind function + * + * @description + * Wrap a string with another string + */ +angular.module('a8m.wrap', []) + .filter('wrap', function () { + return function(input, wrap, ends) { + return isString(input) && isDefined(wrap) + ? [wrap, input, ends || wrap].join('') + : input; + } + }); + +/** + * @ngdoc provider + * @name filterWatcher + * @kind function + * + * @description + * store specific filters result in $$cache, based on scope life time(avoid memory leak). + * on scope.$destroy remove it's cache from $$cache container + */ + +angular.module('a8m.filter-watcher', []) + .provider('filterWatcher', function() { + + this.$get = ['$window', '$rootScope', function($window, $rootScope) { + + /** + * Cache storing + * @type {Object} + */ + var $$cache = {}; + + /** + * Scope listeners container + * scope.$destroy => remove all cache keys + * bind to current scope. + * @type {Object} + */ + var $$listeners = {}; + + /** + * $timeout without triggering the digest cycle + * @type {function} + */ + var $$timeout = $window.setTimeout; + + /** + * @description + * get `HashKey` string based on the given arguments. + * @param fName + * @param args + * @returns {string} + */ + function getHashKey(fName, args) { + function replacerFactory() { + var cache = []; + return function(key, val) { + if(isObject(val) && !isNull(val)) { + if (~cache.indexOf(val)) return '[Circular]'; + cache.push(val) + } + if($window == val) return '$WINDOW'; + if($window.document == val) return '$DOCUMENT'; + if(isScope(val)) return '$SCOPE'; + return val; + } + } + return [fName, JSON.stringify(args, replacerFactory())] + .join('#') + .replace(/"/g,''); + } + + /** + * @description + * fir on $scope.$destroy, + * remove cache based scope from `$$cache`, + * and remove itself from `$$listeners` + * @param event + */ + function removeCache(event) { + var id = event.targetScope.$id; + forEach($$listeners[id], function(key) { + delete $$cache[key]; + }); + delete $$listeners[id]; + } + + /** + * @description + * for angular version that greater than v.1.3.0 + * it clear cache when the digest cycle is end. + */ + function cleanStateless() { + $$timeout(function() { + if(!$rootScope.$$phase) + $$cache = {}; + }, 2000); + } + + /** + * @description + * Store hashKeys in $$listeners container + * on scope.$destroy, remove them all(bind an event). + * @param scope + * @param hashKey + * @returns {*} + */ + function addListener(scope, hashKey) { + var id = scope.$id; + if(isUndefined($$listeners[id])) { + scope.$on('$destroy', removeCache); + $$listeners[id] = []; + } + return $$listeners[id].push(hashKey); + } + + /** + * @description + * return the `cacheKey` or undefined. + * @param filterName + * @param args + * @returns {*} + */ + function $$isMemoized(filterName, args) { + var hashKey = getHashKey(filterName, args); + return $$cache[hashKey]; + } + + /** + * @description + * store `result` in `$$cache` container, based on the hashKey. + * add $destroy listener and return result + * @param filterName + * @param args + * @param scope + * @param result + * @returns {*} + */ + function $$memoize(filterName, args, scope, result) { + var hashKey = getHashKey(filterName, args); + //store result in `$$cache` container + $$cache[hashKey] = result; + // for angular versions that less than 1.3 + // add to `$destroy` listener, a cleaner callback + if(isScope(scope)) { + addListener(scope, hashKey); + } else { + cleanStateless(); + } + return result; + } + + return { + isMemoized: $$isMemoized, + memoize: $$memoize + } + }]; + }); + + +/** + * @ngdoc module + * @name angular.filters + * @description + * Bunch of useful filters for angularJS + */ + +angular.module('angular.filter', [ + + 'a8m.ucfirst', + 'a8m.uri-encode', + 'a8m.uri-component-encode', + 'a8m.slugify', + 'a8m.latinize', + 'a8m.strip-tags', + 'a8m.stringular', + 'a8m.truncate', + 'a8m.starts-with', + 'a8m.ends-with', + 'a8m.wrap', + 'a8m.trim', + 'a8m.ltrim', + 'a8m.rtrim', + 'a8m.repeat', + 'a8m.test', + 'a8m.match', + 'a8m.split', + 'a8m.phoneUS', + + 'a8m.to-array', + 'a8m.concat', + 'a8m.contains', + 'a8m.unique', + 'a8m.is-empty', + 'a8m.after', + 'a8m.after-where', + 'a8m.before', + 'a8m.before-where', + 'a8m.defaults', + 'a8m.where', + 'a8m.reverse', + 'a8m.remove', + 'a8m.remove-with', + 'a8m.group-by', + 'a8m.count-by', + 'a8m.chunk-by', + 'a8m.search-field', + 'a8m.fuzzy-by', + 'a8m.fuzzy', + 'a8m.omit', + 'a8m.pick', + 'a8m.every', + 'a8m.filter-by', + 'a8m.xor', + 'a8m.map', + 'a8m.first', + 'a8m.last', + 'a8m.flatten', + 'a8m.join', + 'a8m.range', + + 'a8m.math.max', + 'a8m.math.min', + 'a8m.math.abs', + 'a8m.math.percent', + 'a8m.math.radix', + 'a8m.math.sum', + 'a8m.math.degrees', + 'a8m.math.radians', + 'a8m.math.byteFmt', + 'a8m.math.kbFmt', + 'a8m.math.shortFmt', + + 'a8m.angular', + 'a8m.conditions', + 'a8m.is-null', + + 'a8m.filter-watcher' +]); +})( window, window.angular ); +/*global define:true*/ +(function(root, factory) { + + 'use strict'; + + if (typeof define === 'function' && define.amd) { + // AMD + define(['angular'], factory); + } else if (typeof exports === 'object') { + // CommonJS + module.exports = factory(require('angular')); + } else { + // Browser, nothing "exported". Only registered as a module with angular. + factory(root.angular); + } +}(this, function(angular) { + + 'use strict'; + + // This returned angular module 'gridster' is what is exported. + return angular.module('gridster', []) + + .constant('gridsterConfig', { + columns: 6, // number of columns in the grid + pushing: true, // whether to push other items out of the way + floating: true, // whether to automatically float items up so they stack + swapping: false, // whether or not to have items switch places instead of push down if they are the same size + width: 'auto', // width of the grid. "auto" will expand the grid to its parent container + colWidth: 'auto', // width of grid columns. "auto" will divide the width of the grid evenly among the columns + rowHeight: 'match', // height of grid rows. 'match' will make it the same as the column width, a numeric value will be interpreted as pixels, '/2' is half the column width, '*5' is five times the column width, etc. + margins: [10, 10], // margins in between grid items + outerMargin: true, + sparse: false, // "true" can increase performance of dragging and resizing for big grid (e.g. 20x50) + isMobile: false, // toggle mobile view + mobileBreakPoint: 600, // width threshold to toggle mobile mode + mobileModeEnabled: true, // whether or not to toggle mobile mode when screen width is less than mobileBreakPoint + minColumns: 1, // minimum amount of columns the grid can scale down to + minRows: 1, // minimum amount of rows to show if the grid is empty + maxRows: 100, // maximum amount of rows in the grid + defaultSizeX: 2, // default width of an item in columns + defaultSizeY: 1, // default height of an item in rows + minSizeX: 1, // minimum column width of an item + maxSizeX: null, // maximum column width of an item + minSizeY: 1, // minumum row height of an item + maxSizeY: null, // maximum row height of an item + saveGridItemCalculatedHeightInMobile: false, // grid item height in mobile display. true- to use the calculated height by sizeY given + resizable: { // options to pass to resizable handler + enabled: true, + handles: ['s', 'e', 'n', 'w', 'se', 'ne', 'sw', 'nw'] + }, + draggable: { // options to pass to draggable handler + enabled: true, + scrollSensitivity: 20, // Distance in pixels from the edge of the viewport after which the viewport should scroll, relative to pointer + scrollSpeed: 15 // Speed at which the window should scroll once the mouse pointer gets within scrollSensitivity distance + } + }) + + .controller('GridsterCtrl', ['gridsterConfig', '$timeout', + function(gridsterConfig, $timeout) { + + var gridster = this; + + /** + * Create options from gridsterConfig constant + */ + angular.extend(this, gridsterConfig); + + this.resizable = angular.extend({}, gridsterConfig.resizable || {}); + this.draggable = angular.extend({}, gridsterConfig.draggable || {}); + + var flag = false; + this.layoutChanged = function() { + if (flag) { + return; + } + flag = true; + $timeout(function() { + flag = false; + if (gridster.loaded) { + gridster.floatItemsUp(); + } + gridster.updateHeight(gridster.movingItem ? gridster.movingItem.sizeY : 0); + }, 30); + }; + + /** + * A positional array of the items in the grid + */ + this.grid = []; + this.allItems = []; + + /** + * Clean up after yourself + */ + this.destroy = function() { + // empty the grid to cut back on the possibility + // of circular references + if (this.grid) { + this.grid = []; + } + this.$element = null; + + if (this.allItems) { + this.allItems.length = 0; + this.allItems = null; + } + }; + + /** + * Overrides default options + * + * @param {Object} options The options to override + */ + this.setOptions = function(options) { + if (!options) { + return; + } + + options = angular.extend({}, options); + + // all this to avoid using jQuery... + if (options.draggable) { + angular.extend(this.draggable, options.draggable); + delete(options.draggable); + } + if (options.resizable) { + angular.extend(this.resizable, options.resizable); + delete(options.resizable); + } + + angular.extend(this, options); + + if (!this.margins || this.margins.length !== 2) { + this.margins = [0, 0]; + } else { + for (var x = 0, l = this.margins.length; x < l; ++x) { + this.margins[x] = parseInt(this.margins[x], 10); + if (isNaN(this.margins[x])) { + this.margins[x] = 0; + } + } + } + }; + + /** + * Check if item can occupy a specified position in the grid + * + * @param {Object} item The item in question + * @param {Number} row The row index + * @param {Number} column The column index + * @returns {Boolean} True if if item fits + */ + this.canItemOccupy = function(item, row, column) { + return row > -1 && column > -1 && item.sizeX + column <= this.columns && item.sizeY + row <= this.maxRows; + }; + + /** + * Set the item in the first suitable position + * + * @param {Object} item The item to insert + */ + this.autoSetItemPosition = function(item) { + // walk through each row and column looking for a place it will fit + for (var rowIndex = 0; rowIndex < this.maxRows; ++rowIndex) { + for (var colIndex = 0; colIndex < this.columns; ++colIndex) { + // only insert if position is not already taken and it can fit + var items = this.getItems(rowIndex, colIndex, item.sizeX, item.sizeY, item); + if (items.length === 0 && this.canItemOccupy(item, rowIndex, colIndex)) { + this.putItem(item, rowIndex, colIndex); + return; + } + } + } + throw new Error('Unable to place item!'); + }; + + /** + * Gets items at a specific coordinate + * + * @param {Number} row + * @param {Number} column + * @param {Number} sizeX + * @param {Number} sizeY + * @param {Array} excludeItems An array of items to exclude from selection + * @returns {Array} Items that match the criteria + */ + this.getItems = function(row, column, sizeX, sizeY, excludeItems) { + var items = []; + if (!sizeX || !sizeY) { + sizeX = sizeY = 1; + } + if (excludeItems && !(excludeItems instanceof Array)) { + excludeItems = [excludeItems]; + } + var item; + if (this.sparse === false) { // check all cells + for (var h = 0; h < sizeY; ++h) { + for (var w = 0; w < sizeX; ++w) { + item = this.getItem(row + h, column + w, excludeItems); + if (item && (!excludeItems || excludeItems.indexOf(item) === -1) && items.indexOf(item) === -1) { + items.push(item); + } + } + } + } else { // check intersection with all items + var bottom = row + sizeY - 1; + var right = column + sizeX - 1; + for (var i = 0; i < this.allItems.length; ++i) { + item = this.allItems[i]; + if (item && (!excludeItems || excludeItems.indexOf(item) === -1) && items.indexOf(item) === -1 && this.intersect(item, column, right, row, bottom)) { + items.push(item); + } + } + } + return items; + }; + + /** + * @param {Array} items + * @returns {Object} An item that represents the bounding box of the items + */ + this.getBoundingBox = function(items) { + + if (items.length === 0) { + return null; + } + if (items.length === 1) { + return { + row: items[0].row, + col: items[0].col, + sizeY: items[0].sizeY, + sizeX: items[0].sizeX + }; + } + + var maxRow = 0; + var maxCol = 0; + var minRow = 9999; + var minCol = 9999; + + for (var i = 0, l = items.length; i < l; ++i) { + var item = items[i]; + minRow = Math.min(item.row, minRow); + minCol = Math.min(item.col, minCol); + maxRow = Math.max(item.row + item.sizeY, maxRow); + maxCol = Math.max(item.col + item.sizeX, maxCol); + } + + return { + row: minRow, + col: minCol, + sizeY: maxRow - minRow, + sizeX: maxCol - minCol + }; + }; + + /** + * Checks if item intersects specified box + * + * @param {object} item + * @param {number} left + * @param {number} right + * @param {number} top + * @param {number} bottom + */ + + this.intersect = function(item, left, right, top, bottom) { + return (left <= item.col + item.sizeX - 1 && + right >= item.col && + top <= item.row + item.sizeY - 1 && + bottom >= item.row); + }; + + + /** + * Removes an item from the grid + * + * @param {Object} item + */ + this.removeItem = function(item) { + var index; + for (var rowIndex = 0, l = this.grid.length; rowIndex < l; ++rowIndex) { + var columns = this.grid[rowIndex]; + if (!columns) { + continue; + } + index = columns.indexOf(item); + if (index !== -1) { + columns[index] = null; + break; + } + } + if (this.sparse) { + index = this.allItems.indexOf(item); + if (index !== -1) { + this.allItems.splice(index, 1); + } + } + this.layoutChanged(); + }; + + /** + * Returns the item at a specified coordinate + * + * @param {Number} row + * @param {Number} column + * @param {Array} excludeItems Items to exclude from selection + * @returns {Object} The matched item or null + */ + this.getItem = function(row, column, excludeItems) { + if (excludeItems && !(excludeItems instanceof Array)) { + excludeItems = [excludeItems]; + } + var sizeY = 1; + while (row > -1) { + var sizeX = 1, + col = column; + while (col > -1) { + var items = this.grid[row]; + if (items) { + var item = items[col]; + if (item && (!excludeItems || excludeItems.indexOf(item) === -1) && item.sizeX >= sizeX && item.sizeY >= sizeY) { + return item; + } + } + ++sizeX; + --col; + } + --row; + ++sizeY; + } + return null; + }; + + /** + * Insert an array of items into the grid + * + * @param {Array} items An array of items to insert + */ + this.putItems = function(items) { + for (var i = 0, l = items.length; i < l; ++i) { + this.putItem(items[i]); + } + }; + + /** + * Insert a single item into the grid + * + * @param {Object} item The item to insert + * @param {Number} row (Optional) Specifies the items row index + * @param {Number} column (Optional) Specifies the items column index + * @param {Array} ignoreItems + */ + this.putItem = function(item, row, column, ignoreItems) { + // auto place item if no row specified + if (typeof row === 'undefined' || row === null) { + row = item.row; + column = item.col; + if (typeof row === 'undefined' || row === null) { + this.autoSetItemPosition(item); + return; + } + } + + // keep item within allowed bounds + if (!this.canItemOccupy(item, row, column)) { + column = Math.min(this.columns - item.sizeX, Math.max(0, column)); + row = Math.min(this.maxRows - item.sizeY, Math.max(0, row)); + } + + // check if item is already in grid + if (item.oldRow !== null && typeof item.oldRow !== 'undefined') { + var samePosition = item.oldRow === row && item.oldColumn === column; + var inGrid = this.grid[row] && this.grid[row][column] === item; + if (samePosition && inGrid) { + item.row = row; + item.col = column; + return; + } else { + // remove from old position + var oldRow = this.grid[item.oldRow]; + if (oldRow && oldRow[item.oldColumn] === item) { + delete oldRow[item.oldColumn]; + } + } + } + + item.oldRow = item.row = row; + item.oldColumn = item.col = column; + + this.moveOverlappingItems(item, ignoreItems); + + if (!this.grid[row]) { + this.grid[row] = []; + } + this.grid[row][column] = item; + + if (this.sparse && this.allItems.indexOf(item) === -1) { + this.allItems.push(item); + } + + if (this.movingItem === item) { + this.floatItemUp(item); + } + this.layoutChanged(); + }; + + /** + * Trade row and column if item1 with item2 + * + * @param {Object} item1 + * @param {Object} item2 + */ + this.swapItems = function(item1, item2) { + this.grid[item1.row][item1.col] = item2; + this.grid[item2.row][item2.col] = item1; + + var item1Row = item1.row; + var item1Col = item1.col; + item1.row = item2.row; + item1.col = item2.col; + item2.row = item1Row; + item2.col = item1Col; + }; + + /** + * Prevents items from being overlapped + * + * @param {Object} item The item that should remain + * @param {Array} ignoreItems + */ + this.moveOverlappingItems = function(item, ignoreItems) { + // don't move item, so ignore it + if (!ignoreItems) { + ignoreItems = [item]; + } else if (ignoreItems.indexOf(item) === -1) { + ignoreItems = ignoreItems.slice(0); + ignoreItems.push(item); + } + + // get the items in the space occupied by the item's coordinates + var overlappingItems = this.getItems( + item.row, + item.col, + item.sizeX, + item.sizeY, + ignoreItems + ); + this.moveItemsDown(overlappingItems, item.row + item.sizeY, ignoreItems); + }; + + /** + * Moves an array of items to a specified row + * + * @param {Array} items The items to move + * @param {Number} newRow The target row + * @param {Array} ignoreItems + */ + this.moveItemsDown = function(items, newRow, ignoreItems) { + if (!items || items.length === 0) { + return; + } + items.sort(function(a, b) { + return a.row - b.row; + }); + + ignoreItems = ignoreItems ? ignoreItems.slice(0) : []; + var topRows = {}, + item, i, l; + + // calculate the top rows in each column + for (i = 0, l = items.length; i < l; ++i) { + item = items[i]; + var topRow = topRows[item.col]; + if (typeof topRow === 'undefined' || item.row < topRow) { + topRows[item.col] = item.row; + } + } + + // move each item down from the top row in its column to the row + for (i = 0, l = items.length; i < l; ++i) { + item = items[i]; + var rowsToMove = newRow - topRows[item.col]; + this.moveItemDown(item, item.row + rowsToMove, ignoreItems); + ignoreItems.push(item); + } + }; + + /** + * Moves an item down to a specified row + * + * @param {Object} item The item to move + * @param {Number} newRow The target row + * @param {Array} ignoreItems + */ + this.moveItemDown = function(item, newRow, ignoreItems) { + if (item.row >= newRow) { + return; + } + while (item.row < newRow) { + ++item.row; + this.moveOverlappingItems(item, ignoreItems); + } + this.putItem(item, item.row, item.col, ignoreItems); + }; + + /** + * Moves all items up as much as possible + */ + this.floatItemsUp = function() { + if (this.floating === false) { + return; + } + for (var rowIndex = 0, l = this.grid.length; rowIndex < l; ++rowIndex) { + var columns = this.grid[rowIndex]; + if (!columns) { + continue; + } + for (var colIndex = 0, len = columns.length; colIndex < len; ++colIndex) { + var item = columns[colIndex]; + if (item) { + this.floatItemUp(item); + } + } + } + }; + + /** + * Float an item up to the most suitable row + * + * @param {Object} item The item to move + */ + this.floatItemUp = function(item) { + if (this.floating === false) { + return; + } + var colIndex = item.col, + sizeY = item.sizeY, + sizeX = item.sizeX, + bestRow = null, + bestColumn = null, + rowIndex = item.row - 1; + + while (rowIndex > -1) { + var items = this.getItems(rowIndex, colIndex, sizeX, sizeY, item); + if (items.length !== 0) { + break; + } + bestRow = rowIndex; + bestColumn = colIndex; + --rowIndex; + } + if (bestRow !== null) { + this.putItem(item, bestRow, bestColumn); + } + }; + + /** + * Update gridsters height + * + * @param {Number} plus (Optional) Additional height to add + */ + this.updateHeight = function(plus) { + var maxHeight = this.minRows; + plus = plus || 0; + for (var rowIndex = this.grid.length; rowIndex >= 0; --rowIndex) { + var columns = this.grid[rowIndex]; + if (!columns) { + continue; + } + for (var colIndex = 0, len = columns.length; colIndex < len; ++colIndex) { + if (columns[colIndex]) { + maxHeight = Math.max(maxHeight, rowIndex + plus + columns[colIndex].sizeY); + } + } + } + this.gridHeight = this.maxRows - maxHeight > 0 ? Math.min(this.maxRows, maxHeight) : Math.max(this.maxRows, maxHeight); + }; + + /** + * Returns the number of rows that will fit in given amount of pixels + * + * @param {Number} pixels + * @param {Boolean} ceilOrFloor (Optional) Determines rounding method + */ + this.pixelsToRows = function(pixels, ceilOrFloor) { + if (!this.outerMargin) { + pixels += this.margins[0] / 2; + } + + if (ceilOrFloor === true) { + return Math.ceil(pixels / this.curRowHeight); + } else if (ceilOrFloor === false) { + return Math.floor(pixels / this.curRowHeight); + } + + return Math.round(pixels / this.curRowHeight); + }; + + /** + * Returns the number of columns that will fit in a given amount of pixels + * + * @param {Number} pixels + * @param {Boolean} ceilOrFloor (Optional) Determines rounding method + * @returns {Number} The number of columns + */ + this.pixelsToColumns = function(pixels, ceilOrFloor) { + if (!this.outerMargin) { + pixels += this.margins[1] / 2; + } + + if (ceilOrFloor === true) { + return Math.ceil(pixels / this.curColWidth); + } else if (ceilOrFloor === false) { + return Math.floor(pixels / this.curColWidth); + } + + return Math.round(pixels / this.curColWidth); + }; + } + ]) + + .directive('gridsterPreview', function() { + return { + replace: true, + scope: true, + require: '^gridster', + template: '
      ', + link: function(scope, $el, attrs, gridster) { + + /** + * @returns {Object} style object for preview element + */ + scope.previewStyle = function() { + if (!gridster.movingItem) { + return { + display: 'none' + }; + } + + return { + display: 'block', + height: (gridster.movingItem.sizeY * gridster.curRowHeight - gridster.margins[0]) + 'px', + width: (gridster.movingItem.sizeX * gridster.curColWidth - gridster.margins[1]) + 'px', + top: (gridster.movingItem.row * gridster.curRowHeight + (gridster.outerMargin ? gridster.margins[0] : 0)) + 'px', + left: (gridster.movingItem.col * gridster.curColWidth + (gridster.outerMargin ? gridster.margins[1] : 0)) + 'px' + }; + }; + } + }; + }) + + /** + * The gridster directive + * + * @param {Function} $timeout + * @param {Object} $window + * @param {Object} $rootScope + * @param {Function} gridsterDebounce + */ + .directive('gridster', ['$timeout', '$window', '$rootScope', 'gridsterDebounce', + function($timeout, $window, $rootScope, gridsterDebounce) { + return { + scope: true, + restrict: 'EAC', + controller: 'GridsterCtrl', + controllerAs: 'gridster', + compile: function($tplElem) { + + $tplElem.prepend('
      '); + + return function(scope, $elem, attrs, gridster) { + gridster.loaded = false; + + gridster.$element = $elem; + + scope.gridster = gridster; + + $elem.addClass('gridster'); + + var isVisible = function(ele) { + return ele.style.visibility !== 'hidden' && ele.style.display !== 'none'; + }; + + function updateHeight() { + $elem.css('height', (gridster.gridHeight * gridster.curRowHeight) + (gridster.outerMargin ? gridster.margins[0] : -gridster.margins[0]) + 'px'); + } + + scope.$watch(function() { + return gridster.gridHeight; + }, updateHeight); + + scope.$watch(function() { + return gridster.movingItem; + }, function() { + gridster.updateHeight(gridster.movingItem ? gridster.movingItem.sizeY : 0); + }); + + function refresh(config) { + gridster.setOptions(config); + + if (!isVisible($elem[0])) { + return; + } + + // resolve "auto" & "match" values + if (gridster.width === 'auto') { + gridster.curWidth = $elem[0].offsetWidth || parseInt($elem.css('width'), 10); + } else { + gridster.curWidth = gridster.width; + } + + if (gridster.colWidth === 'auto') { + gridster.curColWidth = (gridster.curWidth + (gridster.outerMargin ? -gridster.margins[1] : gridster.margins[1])) / gridster.columns; + } else { + gridster.curColWidth = gridster.colWidth; + } + + gridster.curRowHeight = gridster.rowHeight; + if (typeof gridster.rowHeight === 'string') { + if (gridster.rowHeight === 'match') { + gridster.curRowHeight = Math.round(gridster.curColWidth); + } else if (gridster.rowHeight.indexOf('*') !== -1) { + gridster.curRowHeight = Math.round(gridster.curColWidth * gridster.rowHeight.replace('*', '').replace(' ', '')); + } else if (gridster.rowHeight.indexOf('/') !== -1) { + gridster.curRowHeight = Math.round(gridster.curColWidth / gridster.rowHeight.replace('/', '').replace(' ', '')); + } + } + + gridster.isMobile = gridster.mobileModeEnabled && gridster.curWidth <= gridster.mobileBreakPoint; + + // loop through all items and reset their CSS + for (var rowIndex = 0, l = gridster.grid.length; rowIndex < l; ++rowIndex) { + var columns = gridster.grid[rowIndex]; + if (!columns) { + continue; + } + + for (var colIndex = 0, len = columns.length; colIndex < len; ++colIndex) { + if (columns[colIndex]) { + var item = columns[colIndex]; + item.setElementPosition(); + item.setElementSizeY(); + item.setElementSizeX(); + } + } + } + + updateHeight(); + } + + var optionsKey = attrs.gridster; + if (optionsKey) { + scope.$parent.$watch(optionsKey, function(newConfig) { + refresh(newConfig); + }, true); + } else { + refresh({}); + } + + scope.$watch(function() { + return gridster.loaded; + }, function() { + if (gridster.loaded) { + $elem.addClass('gridster-loaded'); + $rootScope.$broadcast('gridster-loaded', gridster); + } else { + $elem.removeClass('gridster-loaded'); + } + }); + + scope.$watch(function() { + return gridster.isMobile; + }, function() { + if (gridster.isMobile) { + $elem.addClass('gridster-mobile').removeClass('gridster-desktop'); + } else { + $elem.removeClass('gridster-mobile').addClass('gridster-desktop'); + } + $rootScope.$broadcast('gridster-mobile-changed', gridster); + }); + + scope.$watch(function() { + return gridster.draggable; + }, function() { + $rootScope.$broadcast('gridster-draggable-changed', gridster); + }, true); + + scope.$watch(function() { + return gridster.resizable; + }, function() { + $rootScope.$broadcast('gridster-resizable-changed', gridster); + }, true); + + var prevWidth = $elem[0].offsetWidth || parseInt($elem.css('width'), 10); + + var resize = function() { + var width = $elem[0].offsetWidth || parseInt($elem.css('width'), 10); + + if (!width || width === prevWidth || gridster.movingItem) { + return; + } + prevWidth = width; + + if (gridster.loaded) { + $elem.removeClass('gridster-loaded'); + } + + refresh(); + + if (gridster.loaded) { + $elem.addClass('gridster-loaded'); + } + + $rootScope.$broadcast('gridster-resized', [width, $elem[0].offsetHeight], gridster); + }; + + // track element width changes any way we can + var onResize = gridsterDebounce(function onResize() { + resize(); + $timeout(function() { + scope.$apply(); + }); + }, 100); + + scope.$watch(function() { + return isVisible($elem[0]); + }, onResize); + + // see https://github.com/sdecima/javascript-detect-element-resize + if (typeof window.addResizeListener === 'function') { + window.addResizeListener($elem[0], onResize); + } else { + scope.$watch(function() { + return $elem[0].offsetWidth || parseInt($elem.css('width'), 10); + }, resize); + } + var $win = angular.element($window); + $win.on('resize', onResize); + + // be sure to cleanup + scope.$on('$destroy', function() { + gridster.destroy(); + $win.off('resize', onResize); + if (typeof window.removeResizeListener === 'function') { + window.removeResizeListener($elem[0], onResize); + } + }); + + // allow a little time to place items before floating up + $timeout(function() { + scope.$watch('gridster.floating', function() { + gridster.floatItemsUp(); + }); + gridster.loaded = true; + }, 100); + }; + } + }; + } + ]) + + .controller('GridsterItemCtrl', function() { + this.$element = null; + this.gridster = null; + this.row = null; + this.col = null; + this.sizeX = null; + this.sizeY = null; + this.minSizeX = 0; + this.minSizeY = 0; + this.maxSizeX = null; + this.maxSizeY = null; + + this.init = function($element, gridster) { + this.$element = $element; + this.gridster = gridster; + this.sizeX = gridster.defaultSizeX; + this.sizeY = gridster.defaultSizeY; + }; + + this.destroy = function() { + // set these to null to avoid the possibility of circular references + this.gridster = null; + this.$element = null; + }; + + /** + * Returns the items most important attributes + */ + this.toJSON = function() { + return { + row: this.row, + col: this.col, + sizeY: this.sizeY, + sizeX: this.sizeX + }; + }; + + this.isMoving = function() { + return this.gridster.movingItem === this; + }; + + /** + * Set the items position + * + * @param {Number} row + * @param {Number} column + */ + this.setPosition = function(row, column) { + this.gridster.putItem(this, row, column); + + if (!this.isMoving()) { + this.setElementPosition(); + } + }; + + /** + * Sets a specified size property + * + * @param {String} key Can be either "x" or "y" + * @param {Number} value The size amount + * @param {Boolean} preventMove + */ + this.setSize = function(key, value, preventMove) { + key = key.toUpperCase(); + var camelCase = 'size' + key, + titleCase = 'Size' + key; + if (value === '') { + return; + } + value = parseInt(value, 10); + if (isNaN(value) || value === 0) { + value = this.gridster['default' + titleCase]; + } + var max = key === 'X' ? this.gridster.columns : this.gridster.maxRows; + if (this['max' + titleCase]) { + max = Math.min(this['max' + titleCase], max); + } + if (this.gridster['max' + titleCase]) { + max = Math.min(this.gridster['max' + titleCase], max); + } + if (key === 'X' && this.cols) { + max -= this.cols; + } else if (key === 'Y' && this.rows) { + max -= this.rows; + } + + var min = 0; + if (this['min' + titleCase]) { + min = Math.max(this['min' + titleCase], min); + } + if (this.gridster['min' + titleCase]) { + min = Math.max(this.gridster['min' + titleCase], min); + } + + value = Math.max(Math.min(value, max), min); + + var changed = (this[camelCase] !== value || (this['old' + titleCase] && this['old' + titleCase] !== value)); + this['old' + titleCase] = this[camelCase] = value; + + if (!this.isMoving()) { + this['setElement' + titleCase](); + } + if (!preventMove && changed) { + this.gridster.moveOverlappingItems(this); + this.gridster.layoutChanged(); + } + + return changed; + }; + + /** + * Sets the items sizeY property + * + * @param {Number} rows + * @param {Boolean} preventMove + */ + this.setSizeY = function(rows, preventMove) { + return this.setSize('Y', rows, preventMove); + }; + + /** + * Sets the items sizeX property + * + * @param {Number} columns + * @param {Boolean} preventMove + */ + this.setSizeX = function(columns, preventMove) { + return this.setSize('X', columns, preventMove); + }; + + /** + * Sets an elements position on the page + */ + this.setElementPosition = function() { + if (this.gridster.isMobile) { + this.$element.css({ + marginLeft: this.gridster.margins[0] + 'px', + marginRight: this.gridster.margins[0] + 'px', + marginTop: this.gridster.margins[1] + 'px', + marginBottom: this.gridster.margins[1] + 'px', + top: '', + left: '' + }); + } else { + this.$element.css({ + margin: 0, + top: (this.row * this.gridster.curRowHeight + (this.gridster.outerMargin ? this.gridster.margins[0] : 0)) + 'px', + left: (this.col * this.gridster.curColWidth + (this.gridster.outerMargin ? this.gridster.margins[1] : 0)) + 'px' + }); + } + }; + + /** + * Sets an elements height + */ + this.setElementSizeY = function() { + if (this.gridster.isMobile && !this.gridster.saveGridItemCalculatedHeightInMobile) { + this.$element.css('height', ''); + } else { + this.$element.css('height', (this.sizeY * this.gridster.curRowHeight - this.gridster.margins[0]) + 'px'); + } + }; + + /** + * Sets an elements width + */ + this.setElementSizeX = function() { + if (this.gridster.isMobile) { + this.$element.css('width', ''); + } else { + this.$element.css('width', (this.sizeX * this.gridster.curColWidth - this.gridster.margins[1]) + 'px'); + } + }; + + /** + * Gets an element's width + */ + this.getElementSizeX = function() { + return (this.sizeX * this.gridster.curColWidth - this.gridster.margins[1]); + }; + + /** + * Gets an element's height + */ + this.getElementSizeY = function() { + return (this.sizeY * this.gridster.curRowHeight - this.gridster.margins[0]); + }; + + }) + + .factory('GridsterTouch', [function() { + return function GridsterTouch(target, startEvent, moveEvent, endEvent) { + var lastXYById = {}; + + // Opera doesn't have Object.keys so we use this wrapper + var numberOfKeys = function(theObject) { + if (Object.keys) { + return Object.keys(theObject).length; + } + + var n = 0, + key; + for (key in theObject) { + ++n; + } + + return n; + }; + + // this calculates the delta needed to convert pageX/Y to offsetX/Y because offsetX/Y don't exist in the TouchEvent object or in Firefox's MouseEvent object + var computeDocumentToElementDelta = function(theElement) { + var elementLeft = 0; + var elementTop = 0; + var oldIEUserAgent = navigator.userAgent.match(/\bMSIE\b/); + + for (var offsetElement = theElement; offsetElement != null; offsetElement = offsetElement.offsetParent) { + // the following is a major hack for versions of IE less than 8 to avoid an apparent problem on the IEBlog with double-counting the offsets + // this may not be a general solution to IE7's problem with offsetLeft/offsetParent + if (oldIEUserAgent && + (!document.documentMode || document.documentMode < 8) && + offsetElement.currentStyle.position === 'relative' && offsetElement.offsetParent && offsetElement.offsetParent.currentStyle.position === 'relative' && offsetElement.offsetLeft === offsetElement.offsetParent.offsetLeft) { + // add only the top + elementTop += offsetElement.offsetTop; + } else { + elementLeft += offsetElement.offsetLeft; + elementTop += offsetElement.offsetTop; + } + } + + return { + x: elementLeft, + y: elementTop + }; + }; + + // cache the delta from the document to our event target (reinitialized each mousedown/MSPointerDown/touchstart) + var documentToTargetDelta = computeDocumentToElementDelta(target); + var useSetReleaseCapture = false; + + // common event handler for the mouse/pointer/touch models and their down/start, move, up/end, and cancel events + var doEvent = function(theEvtObj) { + + if (theEvtObj.type === 'mousemove' && numberOfKeys(lastXYById) === 0) { + return; + } + + var prevent = true; + + var pointerList = theEvtObj.changedTouches ? theEvtObj.changedTouches : [theEvtObj]; + for (var i = 0; i < pointerList.length; ++i) { + var pointerObj = pointerList[i]; + var pointerId = (typeof pointerObj.identifier !== 'undefined') ? pointerObj.identifier : (typeof pointerObj.pointerId !== 'undefined') ? pointerObj.pointerId : 1; + + // use the pageX/Y coordinates to compute target-relative coordinates when we have them (in ie < 9, we need to do a little work to put them there) + if (typeof pointerObj.pageX === 'undefined') { + // initialize assuming our source element is our target + pointerObj.pageX = pointerObj.offsetX + documentToTargetDelta.x; + pointerObj.pageY = pointerObj.offsetY + documentToTargetDelta.y; + + if (pointerObj.srcElement.offsetParent === target && document.documentMode && document.documentMode === 8 && pointerObj.type === 'mousedown') { + // source element is a child piece of VML, we're in IE8, and we've not called setCapture yet - add the origin of the source element + pointerObj.pageX += pointerObj.srcElement.offsetLeft; + pointerObj.pageY += pointerObj.srcElement.offsetTop; + } else if (pointerObj.srcElement !== target && !document.documentMode || document.documentMode < 8) { + // source element isn't the target (most likely it's a child piece of VML) and we're in a version of IE before IE8 - + // the offsetX/Y values are unpredictable so use the clientX/Y values and adjust by the scroll offsets of its parents + // to get the document-relative coordinates (the same as pageX/Y) + var sx = -2, + sy = -2; // adjust for old IE's 2-pixel border + for (var scrollElement = pointerObj.srcElement; scrollElement !== null; scrollElement = scrollElement.parentNode) { + sx += scrollElement.scrollLeft ? scrollElement.scrollLeft : 0; + sy += scrollElement.scrollTop ? scrollElement.scrollTop : 0; + } + + pointerObj.pageX = pointerObj.clientX + sx; + pointerObj.pageY = pointerObj.clientY + sy; + } + } + + + var pageX = pointerObj.pageX; + var pageY = pointerObj.pageY; + + if (theEvtObj.type.match(/(start|down)$/i)) { + // clause for processing MSPointerDown, touchstart, and mousedown + + // refresh the document-to-target delta on start in case the target has moved relative to document + documentToTargetDelta = computeDocumentToElementDelta(target); + + // protect against failing to get an up or end on this pointerId + if (lastXYById[pointerId]) { + if (endEvent) { + endEvent({ + target: theEvtObj.target, + which: theEvtObj.which, + pointerId: pointerId, + pageX: pageX, + pageY: pageY + }); + } + + delete lastXYById[pointerId]; + } + + if (startEvent) { + if (prevent) { + prevent = startEvent({ + target: theEvtObj.target, + which: theEvtObj.which, + pointerId: pointerId, + pageX: pageX, + pageY: pageY + }); + } + } + + // init last page positions for this pointer + lastXYById[pointerId] = { + x: pageX, + y: pageY + }; + + // IE pointer model + if (target.msSetPointerCapture && prevent) { + target.msSetPointerCapture(pointerId); + } else if (theEvtObj.type === 'mousedown' && numberOfKeys(lastXYById) === 1) { + if (useSetReleaseCapture) { + target.setCapture(true); + } else { + document.addEventListener('mousemove', doEvent, false); + document.addEventListener('mouseup', doEvent, false); + } + } + } else if (theEvtObj.type.match(/move$/i)) { + // clause handles mousemove, MSPointerMove, and touchmove + + if (lastXYById[pointerId] && !(lastXYById[pointerId].x === pageX && lastXYById[pointerId].y === pageY)) { + // only extend if the pointer is down and it's not the same as the last point + + if (moveEvent && prevent) { + prevent = moveEvent({ + target: theEvtObj.target, + which: theEvtObj.which, + pointerId: pointerId, + pageX: pageX, + pageY: pageY + }); + } + + // update last page positions for this pointer + lastXYById[pointerId].x = pageX; + lastXYById[pointerId].y = pageY; + } + } else if (lastXYById[pointerId] && theEvtObj.type.match(/(up|end|cancel)$/i)) { + // clause handles up/end/cancel + + if (endEvent && prevent) { + prevent = endEvent({ + target: theEvtObj.target, + which: theEvtObj.which, + pointerId: pointerId, + pageX: pageX, + pageY: pageY + }); + } + + // delete last page positions for this pointer + delete lastXYById[pointerId]; + + // in the Microsoft pointer model, release the capture for this pointer + // in the mouse model, release the capture or remove document-level event handlers if there are no down points + // nothing is required for the iOS touch model because capture is implied on touchstart + if (target.msReleasePointerCapture) { + target.msReleasePointerCapture(pointerId); + } else if (theEvtObj.type === 'mouseup' && numberOfKeys(lastXYById) === 0) { + if (useSetReleaseCapture) { + target.releaseCapture(); + } else { + document.removeEventListener('mousemove', doEvent, false); + document.removeEventListener('mouseup', doEvent, false); + } + } + } + } + + if (prevent) { + if (theEvtObj.preventDefault) { + theEvtObj.preventDefault(); + } + + if (theEvtObj.preventManipulation) { + theEvtObj.preventManipulation(); + } + + if (theEvtObj.preventMouseEvent) { + theEvtObj.preventMouseEvent(); + } + } + }; + + // saving the settings for contentZooming and touchaction before activation + var contentZooming, msTouchAction; + + this.enable = function() { + + if (window.navigator.msPointerEnabled) { + // Microsoft pointer model + target.addEventListener('MSPointerDown', doEvent, false); + target.addEventListener('MSPointerMove', doEvent, false); + target.addEventListener('MSPointerUp', doEvent, false); + target.addEventListener('MSPointerCancel', doEvent, false); + + // css way to prevent panning in our target area + if (typeof target.style.msContentZooming !== 'undefined') { + contentZooming = target.style.msContentZooming; + target.style.msContentZooming = 'none'; + } + + // new in Windows Consumer Preview: css way to prevent all built-in touch actions on our target + // without this, you cannot touch draw on the element because IE will intercept the touch events + if (typeof target.style.msTouchAction !== 'undefined') { + msTouchAction = target.style.msTouchAction; + target.style.msTouchAction = 'none'; + } + } else if (target.addEventListener) { + // iOS touch model + target.addEventListener('touchstart', doEvent, false); + target.addEventListener('touchmove', doEvent, false); + target.addEventListener('touchend', doEvent, false); + target.addEventListener('touchcancel', doEvent, false); + + // mouse model + target.addEventListener('mousedown', doEvent, false); + + // mouse model with capture + // rejecting gecko because, unlike ie, firefox does not send events to target when the mouse is outside target + if (target.setCapture && !window.navigator.userAgent.match(/\bGecko\b/)) { + useSetReleaseCapture = true; + + target.addEventListener('mousemove', doEvent, false); + target.addEventListener('mouseup', doEvent, false); + } + } else if (target.attachEvent && target.setCapture) { + // legacy IE mode - mouse with capture + useSetReleaseCapture = true; + target.attachEvent('onmousedown', function() { + doEvent(window.event); + window.event.returnValue = false; + return false; + }); + target.attachEvent('onmousemove', function() { + doEvent(window.event); + window.event.returnValue = false; + return false; + }); + target.attachEvent('onmouseup', function() { + doEvent(window.event); + window.event.returnValue = false; + return false; + }); + } + }; + + this.disable = function() { + if (window.navigator.msPointerEnabled) { + // Microsoft pointer model + target.removeEventListener('MSPointerDown', doEvent, false); + target.removeEventListener('MSPointerMove', doEvent, false); + target.removeEventListener('MSPointerUp', doEvent, false); + target.removeEventListener('MSPointerCancel', doEvent, false); + + // reset zooming to saved value + if (contentZooming) { + target.style.msContentZooming = contentZooming; + } + + // reset touch action setting + if (msTouchAction) { + target.style.msTouchAction = msTouchAction; + } + } else if (target.removeEventListener) { + // iOS touch model + target.removeEventListener('touchstart', doEvent, false); + target.removeEventListener('touchmove', doEvent, false); + target.removeEventListener('touchend', doEvent, false); + target.removeEventListener('touchcancel', doEvent, false); + + // mouse model + target.removeEventListener('mousedown', doEvent, false); + + // mouse model with capture + // rejecting gecko because, unlike ie, firefox does not send events to target when the mouse is outside target + if (target.setCapture && !window.navigator.userAgent.match(/\bGecko\b/)) { + useSetReleaseCapture = true; + + target.removeEventListener('mousemove', doEvent, false); + target.removeEventListener('mouseup', doEvent, false); + } + } else if (target.detachEvent && target.setCapture) { + // legacy IE mode - mouse with capture + useSetReleaseCapture = true; + target.detachEvent('onmousedown'); + target.detachEvent('onmousemove'); + target.detachEvent('onmouseup'); + } + }; + + return this; + }; + }]) + + .factory('GridsterDraggable', ['$document', '$window', 'GridsterTouch', + function($document, $window, GridsterTouch) { + function GridsterDraggable($el, scope, gridster, item, itemOptions) { + + var elmX, elmY, elmW, elmH, + + mouseX = 0, + mouseY = 0, + lastMouseX = 0, + lastMouseY = 0, + mOffX = 0, + mOffY = 0, + + minTop = 0, + minLeft = 0, + realdocument = $document[0]; + + var originalCol, originalRow; + var inputTags = ['select', 'option', 'input', 'textarea', 'button']; + + function dragStart(event) { + $el.addClass('gridster-item-moving'); + gridster.movingItem = item; + + gridster.updateHeight(item.sizeY); + scope.$apply(function() { + if (gridster.draggable && gridster.draggable.start) { + gridster.draggable.start(event, $el, itemOptions, item); + } + }); + } + + function drag(event) { + var oldRow = item.row, + oldCol = item.col, + hasCallback = gridster.draggable && gridster.draggable.drag, + scrollSensitivity = gridster.draggable.scrollSensitivity, + scrollSpeed = gridster.draggable.scrollSpeed; + + var row = Math.min(gridster.pixelsToRows(elmY), gridster.maxRows - 1); + var col = Math.min(gridster.pixelsToColumns(elmX), gridster.columns - 1); + + var itemsInTheWay = gridster.getItems(row, col, item.sizeX, item.sizeY, item); + var hasItemsInTheWay = itemsInTheWay.length !== 0; + + if (gridster.swapping === true && hasItemsInTheWay) { + var boundingBoxItem = gridster.getBoundingBox(itemsInTheWay), + sameSize = boundingBoxItem.sizeX === item.sizeX && boundingBoxItem.sizeY === item.sizeY, + sameRow = boundingBoxItem.row === oldRow, + sameCol = boundingBoxItem.col === oldCol, + samePosition = boundingBoxItem.row === row && boundingBoxItem.col === col, + inline = sameRow || sameCol; + + if (sameSize && itemsInTheWay.length === 1) { + if (samePosition) { + gridster.swapItems(item, itemsInTheWay[0]); + } else if (inline) { + return; + } + } else if (boundingBoxItem.sizeX <= item.sizeX && boundingBoxItem.sizeY <= item.sizeY && inline) { + var emptyRow = item.row <= row ? item.row : row + item.sizeY, + emptyCol = item.col <= col ? item.col : col + item.sizeX, + rowOffset = emptyRow - boundingBoxItem.row, + colOffset = emptyCol - boundingBoxItem.col; + + for (var i = 0, l = itemsInTheWay.length; i < l; ++i) { + var itemInTheWay = itemsInTheWay[i]; + + var itemsInFreeSpace = gridster.getItems( + itemInTheWay.row + rowOffset, + itemInTheWay.col + colOffset, + itemInTheWay.sizeX, + itemInTheWay.sizeY, + item + ); + + if (itemsInFreeSpace.length === 0) { + gridster.putItem(itemInTheWay, itemInTheWay.row + rowOffset, itemInTheWay.col + colOffset); + } + } + } + } + + if (gridster.pushing !== false || !hasItemsInTheWay) { + item.row = row; + item.col = col; + } + + if (event.pageY - realdocument.body.scrollTop < scrollSensitivity) { + realdocument.body.scrollTop = realdocument.body.scrollTop - scrollSpeed; + } else if ($window.innerHeight - (event.pageY - realdocument.body.scrollTop) < scrollSensitivity) { + realdocument.body.scrollTop = realdocument.body.scrollTop + scrollSpeed; + } + + if (event.pageX - realdocument.body.scrollLeft < scrollSensitivity) { + realdocument.body.scrollLeft = realdocument.body.scrollLeft - scrollSpeed; + } else if ($window.innerWidth - (event.pageX - realdocument.body.scrollLeft) < scrollSensitivity) { + realdocument.body.scrollLeft = realdocument.body.scrollLeft + scrollSpeed; + } + + if (hasCallback || oldRow !== item.row || oldCol !== item.col) { + scope.$apply(function() { + if (hasCallback) { + gridster.draggable.drag(event, $el, itemOptions, item); + } + }); + } + } + + function dragStop(event) { + $el.removeClass('gridster-item-moving'); + var row = Math.min(gridster.pixelsToRows(elmY), gridster.maxRows - 1); + var col = Math.min(gridster.pixelsToColumns(elmX), gridster.columns - 1); + if (gridster.pushing !== false || gridster.getItems(row, col, item.sizeX, item.sizeY, item).length === 0) { + item.row = row; + item.col = col; + } + gridster.movingItem = null; + item.setPosition(item.row, item.col); + + scope.$apply(function() { + if (gridster.draggable && gridster.draggable.stop) { + gridster.draggable.stop(event, $el, itemOptions, item); + } + }); + } + + function mouseDown(e) { + if (inputTags.indexOf(e.target.nodeName.toLowerCase()) !== -1) { + return false; + } + + var $target = angular.element(e.target); + + // exit, if a resize handle was hit + if ($target.hasClass('gridster-item-resizable-handler')) { + return false; + } + + // exit, if the target has it's own click event + if ($target.attr('onclick') || $target.attr('ng-click')) { + return false; + } + + // only works if you have jQuery + if ($target.closest && $target.closest('.gridster-no-drag').length) { + return false; + } + + // apply drag handle filter + if (gridster.draggable && gridster.draggable.handle) { + var $dragHandles = angular.element($el[0].querySelectorAll(gridster.draggable.handle)); + var match = false; + outerloop: + for (var h = 0, hl = $dragHandles.length; h < hl; ++h) { + var handle = $dragHandles[h]; + if (handle === e.target) { + match = true; + break; + } + var target = e.target; + for (var p = 0; p < 20; ++p) { + var parent = target.parentNode; + if (parent === $el[0] || !parent) { + break; + } + if (parent === handle) { + match = true; + break outerloop; + } + target = parent; + } + } + if (!match) { + return false; + } + } + + switch (e.which) { + case 1: + // left mouse button + break; + case 2: + case 3: + // right or middle mouse button + return; + } + + lastMouseX = e.pageX; + lastMouseY = e.pageY; + + elmX = parseInt($el.css('left'), 10); + elmY = parseInt($el.css('top'), 10); + elmW = $el[0].offsetWidth; + elmH = $el[0].offsetHeight; + + originalCol = item.col; + originalRow = item.row; + + dragStart(e); + + return true; + } + + function mouseMove(e) { + if (!$el.hasClass('gridster-item-moving') || $el.hasClass('gridster-item-resizing')) { + return false; + } + + var maxLeft = gridster.curWidth - 1; + var maxTop = gridster.curRowHeight * gridster.maxRows - 1; + + // Get the current mouse position. + mouseX = e.pageX; + mouseY = e.pageY; + + // Get the deltas + var diffX = mouseX - lastMouseX + mOffX; + var diffY = mouseY - lastMouseY + mOffY; + mOffX = mOffY = 0; + + // Update last processed mouse positions. + lastMouseX = mouseX; + lastMouseY = mouseY; + + var dX = diffX, + dY = diffY; + if (elmX + dX < minLeft) { + diffX = minLeft - elmX; + mOffX = dX - diffX; + } else if (elmX + elmW + dX > maxLeft) { + diffX = maxLeft - elmX - elmW; + mOffX = dX - diffX; + } + + if (elmY + dY < minTop) { + diffY = minTop - elmY; + mOffY = dY - diffY; + } else if (elmY + elmH + dY > maxTop) { + diffY = maxTop - elmY - elmH; + mOffY = dY - diffY; + } + elmX += diffX; + elmY += diffY; + + // set new position + $el.css({ + 'top': elmY + 'px', + 'left': elmX + 'px' + }); + + drag(e); + + return true; + } + + function mouseUp(e) { + if (!$el.hasClass('gridster-item-moving') || $el.hasClass('gridster-item-resizing')) { + return false; + } + + mOffX = mOffY = 0; + + dragStop(e); + + return true; + } + + var enabled = null; + var gridsterTouch = null; + + this.enable = function() { + if (enabled === true) { + return; + } + enabled = true; + + if (gridsterTouch) { + gridsterTouch.enable(); + return; + } + + gridsterTouch = new GridsterTouch($el[0], mouseDown, mouseMove, mouseUp); + gridsterTouch.enable(); + }; + + this.disable = function() { + if (enabled === false) { + return; + } + + enabled = false; + if (gridsterTouch) { + gridsterTouch.disable(); + } + }; + + this.toggle = function(enabled) { + if (enabled) { + this.enable(); + } else { + this.disable(); + } + }; + + this.destroy = function() { + this.disable(); + }; + } + + return GridsterDraggable; + } + ]) + + .factory('GridsterResizable', ['GridsterTouch', function(GridsterTouch) { + function GridsterResizable($el, scope, gridster, item, itemOptions) { + + function ResizeHandle(handleClass) { + + var hClass = handleClass; + + var elmX, elmY, elmW, elmH, + + mouseX = 0, + mouseY = 0, + lastMouseX = 0, + lastMouseY = 0, + mOffX = 0, + mOffY = 0, + + minTop = 0, + maxTop = 9999, + minLeft = 0; + + var getMinHeight = function() { + return (item.minSizeY ? item.minSizeY : 1) * gridster.curRowHeight - gridster.margins[0]; + }; + var getMinWidth = function() { + return (item.minSizeX ? item.minSizeX : 1) * gridster.curColWidth - gridster.margins[1]; + }; + + var originalWidth, originalHeight; + var savedDraggable; + + function resizeStart(e) { + $el.addClass('gridster-item-moving'); + $el.addClass('gridster-item-resizing'); + + gridster.movingItem = item; + + item.setElementSizeX(); + item.setElementSizeY(); + item.setElementPosition(); + gridster.updateHeight(1); + + scope.$apply(function() { + // callback + if (gridster.resizable && gridster.resizable.start) { + gridster.resizable.start(e, $el, itemOptions, item); // options is the item model + } + }); + } + + function resize(e) { + var oldRow = item.row, + oldCol = item.col, + oldSizeX = item.sizeX, + oldSizeY = item.sizeY, + hasCallback = gridster.resizable && gridster.resizable.resize; + + var col = item.col; + // only change column if grabbing left edge + if (['w', 'nw', 'sw'].indexOf(handleClass) !== -1) { + col = gridster.pixelsToColumns(elmX, false); + } + + var row = item.row; + // only change row if grabbing top edge + if (['n', 'ne', 'nw'].indexOf(handleClass) !== -1) { + row = gridster.pixelsToRows(elmY, false); + } + + var sizeX = item.sizeX; + // only change row if grabbing left or right edge + if (['n', 's'].indexOf(handleClass) === -1) { + sizeX = gridster.pixelsToColumns(elmW, true); + } + + var sizeY = item.sizeY; + // only change row if grabbing top or bottom edge + if (['e', 'w'].indexOf(handleClass) === -1) { + sizeY = gridster.pixelsToRows(elmH, true); + } + + + var canOccupy = row > -1 && col > -1 && sizeX + col <= gridster.columns && sizeY + row <= gridster.maxRows; + if (canOccupy && (gridster.pushing !== false || gridster.getItems(row, col, sizeX, sizeY, item).length === 0)) { + item.row = row; + item.col = col; + item.sizeX = sizeX; + item.sizeY = sizeY; + } + var isChanged = item.row !== oldRow || item.col !== oldCol || item.sizeX !== oldSizeX || item.sizeY !== oldSizeY; + + if (hasCallback || isChanged) { + scope.$apply(function() { + if (hasCallback) { + gridster.resizable.resize(e, $el, itemOptions, item); // options is the item model + } + }); + } + } + + function resizeStop(e) { + $el.removeClass('gridster-item-moving'); + $el.removeClass('gridster-item-resizing'); + + gridster.movingItem = null; + + item.setPosition(item.row, item.col); + item.setSizeY(item.sizeY); + item.setSizeX(item.sizeX); + + scope.$apply(function() { + if (gridster.resizable && gridster.resizable.stop) { + gridster.resizable.stop(e, $el, itemOptions, item); // options is the item model + } + }); + } + + function mouseDown(e) { + switch (e.which) { + case 1: + // left mouse button + break; + case 2: + case 3: + // right or middle mouse button + return; + } + + // save the draggable setting to restore after resize + savedDraggable = gridster.draggable.enabled; + if (savedDraggable) { + gridster.draggable.enabled = false; + scope.$broadcast('gridster-draggable-changed', gridster); + } + + // Get the current mouse position. + lastMouseX = e.pageX; + lastMouseY = e.pageY; + + // Record current widget dimensions + elmX = parseInt($el.css('left'), 10); + elmY = parseInt($el.css('top'), 10); + elmW = $el[0].offsetWidth; + elmH = $el[0].offsetHeight; + + originalWidth = item.sizeX; + originalHeight = item.sizeY; + + resizeStart(e); + + return true; + } + + function mouseMove(e) { + var maxLeft = gridster.curWidth - 1; + + // Get the current mouse position. + mouseX = e.pageX; + mouseY = e.pageY; + + // Get the deltas + var diffX = mouseX - lastMouseX + mOffX; + var diffY = mouseY - lastMouseY + mOffY; + mOffX = mOffY = 0; + + // Update last processed mouse positions. + lastMouseX = mouseX; + lastMouseY = mouseY; + + var dY = diffY, + dX = diffX; + + if (hClass.indexOf('n') >= 0) { + if (elmH - dY < getMinHeight()) { + diffY = elmH - getMinHeight(); + mOffY = dY - diffY; + } else if (elmY + dY < minTop) { + diffY = minTop - elmY; + mOffY = dY - diffY; + } + elmY += diffY; + elmH -= diffY; + } + if (hClass.indexOf('s') >= 0) { + if (elmH + dY < getMinHeight()) { + diffY = getMinHeight() - elmH; + mOffY = dY - diffY; + } else if (elmY + elmH + dY > maxTop) { + diffY = maxTop - elmY - elmH; + mOffY = dY - diffY; + } + elmH += diffY; + } + if (hClass.indexOf('w') >= 0) { + if (elmW - dX < getMinWidth()) { + diffX = elmW - getMinWidth(); + mOffX = dX - diffX; + } else if (elmX + dX < minLeft) { + diffX = minLeft - elmX; + mOffX = dX - diffX; + } + elmX += diffX; + elmW -= diffX; + } + if (hClass.indexOf('e') >= 0) { + if (elmW + dX < getMinWidth()) { + diffX = getMinWidth() - elmW; + mOffX = dX - diffX; + } else if (elmX + elmW + dX > maxLeft) { + diffX = maxLeft - elmX - elmW; + mOffX = dX - diffX; + } + elmW += diffX; + } + + // set new position + $el.css({ + 'top': elmY + 'px', + 'left': elmX + 'px', + 'width': elmW + 'px', + 'height': elmH + 'px' + }); + + resize(e); + + return true; + } + + function mouseUp(e) { + // restore draggable setting to its original state + if (gridster.draggable.enabled !== savedDraggable) { + gridster.draggable.enabled = savedDraggable; + scope.$broadcast('gridster-draggable-changed', gridster); + } + + mOffX = mOffY = 0; + + resizeStop(e); + + return true; + } + + var $dragHandle = null; + var unifiedInput; + + this.enable = function() { + if (!$dragHandle) { + $dragHandle = angular.element('
      '); + $el.append($dragHandle); + } + + unifiedInput = new GridsterTouch($dragHandle[0], mouseDown, mouseMove, mouseUp); + unifiedInput.enable(); + }; + + this.disable = function() { + if ($dragHandle) { + $dragHandle.remove(); + $dragHandle = null; + } + + unifiedInput.disable(); + unifiedInput = undefined; + }; + + this.destroy = function() { + this.disable(); + }; + } + + var handles = []; + var handlesOpts = gridster.resizable.handles; + if (typeof handlesOpts === 'string') { + handlesOpts = gridster.resizable.handles.split(','); + } + var enabled = false; + + for (var c = 0, l = handlesOpts.length; c < l; c++) { + handles.push(new ResizeHandle(handlesOpts[c])); + } + + this.enable = function() { + if (enabled) { + return; + } + for (var c = 0, l = handles.length; c < l; c++) { + handles[c].enable(); + } + enabled = true; + }; + + this.disable = function() { + if (!enabled) { + return; + } + for (var c = 0, l = handles.length; c < l; c++) { + handles[c].disable(); + } + enabled = false; + }; + + this.toggle = function(enabled) { + if (enabled) { + this.enable(); + } else { + this.disable(); + } + }; + + this.destroy = function() { + for (var c = 0, l = handles.length; c < l; c++) { + handles[c].destroy(); + } + }; + } + return GridsterResizable; + }]) + + .factory('gridsterDebounce', function() { + return function gridsterDebounce(func, wait, immediate) { + var timeout; + return function() { + var context = this, + args = arguments; + var later = function() { + timeout = null; + if (!immediate) { + func.apply(context, args); + } + }; + var callNow = immediate && !timeout; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + if (callNow) { + func.apply(context, args); + } + }; + }; + }) + + /** + * GridsterItem directive + * @param $parse + * @param GridsterDraggable + * @param GridsterResizable + * @param gridsterDebounce + */ + .directive('gridsterItem', ['$parse', 'GridsterDraggable', 'GridsterResizable', 'gridsterDebounce', + function($parse, GridsterDraggable, GridsterResizable, gridsterDebounce) { + return { + scope: true, + restrict: 'EA', + controller: 'GridsterItemCtrl', + controllerAs: 'gridsterItem', + require: ['^gridster', 'gridsterItem'], + link: function(scope, $el, attrs, controllers) { + var optionsKey = attrs.gridsterItem, + options; + + var gridster = controllers[0], + item = controllers[1]; + + scope.gridster = gridster; + + // bind the item's position properties + // options can be an object specified by gridster-item="object" + // or the options can be the element html attributes object + if (optionsKey) { + var $optionsGetter = $parse(optionsKey); + options = $optionsGetter(scope) || {}; + if (!options && $optionsGetter.assign) { + options = { + row: item.row, + col: item.col, + sizeX: item.sizeX, + sizeY: item.sizeY, + minSizeX: 0, + minSizeY: 0, + maxSizeX: null, + maxSizeY: null + }; + $optionsGetter.assign(scope, options); + } + } else { + options = attrs; + } + + item.init($el, gridster); + + $el.addClass('gridster-item'); + + var aspects = ['minSizeX', 'maxSizeX', 'minSizeY', 'maxSizeY', 'sizeX', 'sizeY', 'row', 'col'], + $getters = {}; + + var expressions = []; + var aspectFn = function(aspect) { + var expression; + if (typeof options[aspect] === 'string') { + // watch the expression in the scope + expression = options[aspect]; + } else if (typeof options[aspect.toLowerCase()] === 'string') { + // watch the expression in the scope + expression = options[aspect.toLowerCase()]; + } else if (optionsKey) { + // watch the expression on the options object in the scope + expression = optionsKey + '.' + aspect; + } else { + return; + } + expressions.push('"' + aspect + '":' + expression); + $getters[aspect] = $parse(expression); + + // initial set + var val = $getters[aspect](scope); + if (typeof val === 'number') { + item[aspect] = val; + } + }; + + for (var i = 0, l = aspects.length; i < l; ++i) { + aspectFn(aspects[i]); + } + + var watchExpressions = '{' + expressions.join(',') + '}'; + // when the value changes externally, update the internal item object + scope.$watchCollection(watchExpressions, function(newVals, oldVals) { + for (var aspect in newVals) { + var newVal = newVals[aspect]; + var oldVal = oldVals[aspect]; + if (oldVal === newVal) { + continue; + } + newVal = parseInt(newVal, 10); + if (!isNaN(newVal)) { + item[aspect] = newVal; + } + } + }); + + function positionChanged() { + // call setPosition so the element and gridster controller are updated + item.setPosition(item.row, item.col); + + // when internal item position changes, update externally bound values + if ($getters.row && $getters.row.assign) { + $getters.row.assign(scope, item.row); + } + if ($getters.col && $getters.col.assign) { + $getters.col.assign(scope, item.col); + } + } + scope.$watch(function() { + return item.row + ',' + item.col; + }, positionChanged); + + function sizeChanged() { + var changedX = item.setSizeX(item.sizeX, true); + if (changedX && $getters.sizeX && $getters.sizeX.assign) { + $getters.sizeX.assign(scope, item.sizeX); + } + var changedY = item.setSizeY(item.sizeY, true); + if (changedY && $getters.sizeY && $getters.sizeY.assign) { + $getters.sizeY.assign(scope, item.sizeY); + } + + if (changedX || changedY) { + item.gridster.moveOverlappingItems(item); + gridster.layoutChanged(); + scope.$broadcast('gridster-item-resized', item); + } + } + + scope.$watch(function() { + return item.sizeY + ',' + item.sizeX + ',' + item.minSizeX + ',' + item.maxSizeX + ',' + item.minSizeY + ',' + item.maxSizeY; + }, sizeChanged); + + var draggable = new GridsterDraggable($el, scope, gridster, item, options); + var resizable = new GridsterResizable($el, scope, gridster, item, options); + + var updateResizable = function() { + resizable.toggle(!gridster.isMobile && gridster.resizable && gridster.resizable.enabled); + }; + updateResizable(); + + var updateDraggable = function() { + draggable.toggle(!gridster.isMobile && gridster.draggable && gridster.draggable.enabled); + }; + updateDraggable(); + + scope.$on('gridster-draggable-changed', updateDraggable); + scope.$on('gridster-resizable-changed', updateResizable); + scope.$on('gridster-resized', updateResizable); + scope.$on('gridster-mobile-changed', function() { + updateResizable(); + updateDraggable(); + }); + + function whichTransitionEvent() { + var el = document.createElement('div'); + var transitions = { + 'transition': 'transitionend', + 'OTransition': 'oTransitionEnd', + 'MozTransition': 'transitionend', + 'WebkitTransition': 'webkitTransitionEnd' + }; + for (var t in transitions) { + if (el.style[t] !== undefined) { + return transitions[t]; + } + } + } + + var debouncedTransitionEndPublisher = gridsterDebounce(function() { + scope.$apply(function() { + scope.$broadcast('gridster-item-transition-end', item); + }); + }, 50); + + $el.on(whichTransitionEvent(), debouncedTransitionEndPublisher); + + scope.$broadcast('gridster-item-initialized', item); + + return scope.$on('$destroy', function() { + try { + resizable.destroy(); + draggable.destroy(); + } catch (e) {} + + try { + gridster.removeItem(item); + } catch (e) {} + + try { + item.destroy(); + } catch (e) {} + }); + } + }; + } + ]) + + .directive('gridsterNoDrag', function() { + return { + restrict: 'A', + link: function(scope, $element) { + $element.addClass('gridster-no-drag'); + } + }; + }) + + ; + +})); + +/* + * Angular Material Data Table + * https://github.com/daniel-nagy/md-data-table + * @license MIT + * v0.10.9 + */ +(function (window, angular, undefined) { +'use strict'; + +angular.module('md.table.templates', ['md-table-pagination.html', 'md-table-progress.html', 'arrow-up.svg', 'navigate-before.svg', 'navigate-first.svg', 'navigate-last.svg', 'navigate-next.svg']); + +angular.module('md-table-pagination.html', []).run(['$templateCache', function($templateCache) { + $templateCache.put('md-table-pagination.html', + '
      \n' + + '
      {{$pagination.label.page}}
      \n' + + '\n' + + ' \n' + + ' \n' + + ' {{page}}\n' + + ' \n' + + ' \n' + + '
      \n' + + '\n' + + '
      \n' + + '
      {{$pagination.label.rowsPerPage}}
      \n' + + '\n' + + ' \n' + + ' {{::option.label ? option.label : option}}\n' + + ' \n' + + '
      \n' + + '\n' + + '
      \n' + + '
      {{$pagination.min()}} - {{$pagination.max()}} {{$pagination.label.of}} {{$pagination.total}}
      \n' + + '\n' + + ' \n' + + ' \n' + + ' \n' + + '\n' + + ' \n' + + ' \n' + + ' \n' + + '\n' + + ' \n' + + ' \n' + + ' \n' + + '\n' + + ' \n' + + ' \n' + + ' \n' + + '
      '); +}]); + +angular.module('md-table-progress.html', []).run(['$templateCache', function($templateCache) { + $templateCache.put('md-table-progress.html', + '\n' + + ' \n' + + ' \n' + + ' \n' + + ''); +}]); + +angular.module('arrow-up.svg', []).run(['$templateCache', function($templateCache) { + $templateCache.put('arrow-up.svg', + ''); +}]); + +angular.module('navigate-before.svg', []).run(['$templateCache', function($templateCache) { + $templateCache.put('navigate-before.svg', + ''); +}]); + +angular.module('navigate-first.svg', []).run(['$templateCache', function($templateCache) { + $templateCache.put('navigate-first.svg', + ''); +}]); + +angular.module('navigate-last.svg', []).run(['$templateCache', function($templateCache) { + $templateCache.put('navigate-last.svg', + ''); +}]); + +angular.module('navigate-next.svg', []).run(['$templateCache', function($templateCache) { + $templateCache.put('navigate-next.svg', + ''); +}]); + + +angular.module('md.data.table', ['md.table.templates']); + +angular.module('md.data.table').directive('mdBody', mdBody); + +function mdBody() { + + function compile(tElement) { + tElement.addClass('md-body'); + } + + return { + compile: compile, + restrict: 'A' + }; +} + +angular.module('md.data.table').directive('mdCell', mdCell); + +function mdCell() { + + function compile(tElement) { + var select = tElement.find('md-select'); + + if(select.length) { + select.addClass('md-table-select').attr('md-container-class', 'md-table-select'); + } + + tElement.addClass('md-cell'); + + return postLink; + } + + // empty controller to be bind properties to in postLink function + function Controller() { + + } + + function postLink(scope, element, attrs, ctrls) { + var select = element.find('md-select'); + var cellCtrl = ctrls.shift(); + var tableCtrl = ctrls.shift(); + + if(attrs.ngClick) { + element.addClass('md-clickable'); + } + + if(select.length) { + select.on('click', function (event) { + event.stopPropagation(); + }); + + element.addClass('md-clickable').on('click', function (event) { + event.stopPropagation(); + select[0].click(); + }); + } + + cellCtrl.getTable = tableCtrl.getElement; + + function getColumn() { + return tableCtrl.$$columns[getIndex()]; + } + + function getIndex() { + return Array.prototype.indexOf.call(element.parent().children(), element[0]); + } + + scope.$watch(getColumn, function (column) { + if(!column) { + return; + } + + if(column.numeric) { + element.addClass('md-numeric'); + } else { + element.removeClass('md-numeric'); + } + }); + } + + return { + controller: Controller, + compile: compile, + require: ['mdCell', '^^mdTable'], + restrict: 'A' + }; +} + +angular.module('md.data.table').directive('mdColumn', mdColumn); + +function mdColumn($compile, $mdUtil) { + + function compile(tElement) { + tElement.addClass('md-column'); + return postLink; + } + + function postLink(scope, element, attrs, ctrls) { + var headCtrl = ctrls.shift(); + var tableCtrl = ctrls.shift(); + + function attachSortIcon() { + var sortIcon = angular.element(''); + + $compile(sortIcon.addClass('md-sort-icon').attr('ng-class', 'getDirection()'))(scope); + + if(element.hasClass('md-numeric')) { + element.prepend(sortIcon); + } else { + element.append(sortIcon); + } + } + + function detachSortIcon() { + Array.prototype.some.call(element.find('md-icon'), function (icon) { + return icon.classList.contains('md-sort-icon') && element[0].removeChild(icon); + }); + } + + function disableSorting() { + detachSortIcon(); + element.removeClass('md-sort').off('click', setOrder); + } + + function enableSorting() { + attachSortIcon(); + element.addClass('md-sort').on('click', setOrder); + } + + function getIndex() { + return Array.prototype.indexOf.call(element.parent().children(), element[0]); + } + + function isActive() { + return scope.orderBy && (headCtrl.order === scope.orderBy || headCtrl.order === '-' + scope.orderBy); + } + + function isNumeric() { + return attrs.mdNumeric === '' || scope.numeric; + } + + function setOrder() { + scope.$applyAsync(function () { + if(isActive()) { + headCtrl.order = scope.getDirection() === 'md-asc' ? '-' + scope.orderBy : scope.orderBy; + } else { + headCtrl.order = scope.getDirection() === 'md-asc' ? scope.orderBy : '-' + scope.orderBy; + } + + if(angular.isFunction(headCtrl.onReorder)) { + $mdUtil.nextTick(function () { + headCtrl.onReorder(headCtrl.order); + }); + } + }); + } + + function updateColumn(index, column) { + tableCtrl.$$columns[index] = column; + + if(column.numeric) { + element.addClass('md-numeric'); + } else { + element.removeClass('md-numeric'); + } + } + + scope.getDirection = function () { + if(isActive()) { + return headCtrl.order.charAt(0) === '-' ? 'md-desc' : 'md-asc'; + } + + return attrs.mdDesc === '' || scope.$eval(attrs.mdDesc) ? 'md-desc' : 'md-asc'; + }; + + scope.$watch(isActive, function (active) { + if(active) { + element.addClass('md-active'); + } else { + element.removeClass('md-active'); + } + }); + + scope.$watch(getIndex, function (index) { + updateColumn(index, {'numeric': isNumeric()}); + }); + + scope.$watch(isNumeric, function (numeric) { + updateColumn(getIndex(), {'numeric': numeric}); + }); + + scope.$watch('orderBy', function (orderBy) { + if(orderBy) { + if(!element.hasClass('md-sort')) { + enableSorting(); + } + } else if(element.hasClass('md-sort')) { + disableSorting(); + } + }); + } + + return { + compile: compile, + require: ['^^mdHead', '^^mdTable'], + restrict: 'A', + scope: { + numeric: '=?mdNumeric', + orderBy: '@?mdOrderBy' + } + }; +} + +mdColumn.$inject = ['$compile', '$mdUtil']; + +angular.module('md.data.table') + .decorator('$controller', controllerDecorator) + .factory('$mdEditDialog', mdEditDialog); + +/* + * A decorator for ng.$controller to optionally bind properties to the + * controller before invoking the constructor. Stolen from the ngMock. + * + * https://docs.angularjs.org/api/ngMock/service/$controller + */ +function controllerDecorator($delegate) { + return function(expression, locals, later, ident) { + if(later && typeof later === 'object') { + var create = $delegate(expression, locals, true, ident); + angular.extend(create.instance, later); + return create(); + } + return $delegate(expression, locals, later, ident); + }; +} + +controllerDecorator.$inject = ['$delegate']; + +function mdEditDialog($compile, $controller, $document, $mdUtil, $q, $rootScope, $templateCache, $templateRequest, $window) { + /* jshint validthis: true */ + + var ESCAPE = 27; + + var busy = false; + var body = angular.element($document.prop('body')); + + /* + * bindToController + * controller + * controllerAs + * locals + * resolve + * scope + * targetEvent + * template + * templateUrl + */ + var defaultOptions = { + clickOutsideToClose: true, + disableScroll: true, + escToClose: true, + focusOnOpen: true + }; + + function build(template, options) { + var scope = $rootScope.$new(); + var element = $compile(template)(scope); + var backdrop = $mdUtil.createBackdrop(scope, 'md-edit-dialog-backdrop'); + var controller; + + if(options.controller) { + controller = getController(options, scope, {$element: element, $scope: scope}); + } else { + angular.extend(scope, options.scope); + } + + if(options.disableScroll) { + disableScroll(element); + } + + body.prepend(backdrop).append(element.addClass('md-whiteframe-1dp')); + + positionDialog(element, options.target); + + if(options.focusOnOpen) { + focusOnOpen(element); + } + + if(options.clickOutsideToClose) { + backdrop.on('click', function () { + element.remove(); + }); + } + + if(options.escToClose) { + escToClose(element); + } + + element.on('$destroy', function () { + busy = false; + backdrop.remove(); + }); + + return controller; + } + + function disableScroll(element) { + var restoreScroll = $mdUtil.disableScrollAround(element, body); + + element.on('$destroy', function () { + restoreScroll(); + }); + } + + function getController(options, scope, inject) { + if(!options.controller) { + return; + } + + if(options.resolve) { + angular.extend(inject, options.resolve); + } + + if(options.locals) { + angular.extend(inject, options.locals); + } + + if(options.controllerAs) { + scope[options.controllerAs] = {}; + + if(options.bindToController) { + angular.extend(scope[options.controllerAs], options.scope); + } else { + angular.extend(scope, options.scope); + } + } else { + angular.extend(scope, options.scope); + } + + if(options.bindToController) { + return $controller(options.controller, inject, scope[options.controllerAs]); + } else { + return $controller(options.controller, inject); + } + } + + function getTemplate(options) { + return $q(function (resolve, reject) { + var template = options.template; + + function illegalType(type) { + reject('Unexpected template value. Expected a string; received a ' + type + '.'); + } + + if(template) { + return angular.isString(template) ? resolve(template) : illegalType(typeof template); + } + + if(options.templateUrl) { + template = $templateCache.get(options.templateUrl); + + if(template) { + return resolve(template); + } + + var success = function (template) { + return resolve(template); + }; + + var error = function () { + return reject('Error retrieving template from URL.'); + }; + + return $templateRequest(options.templateUrl).then(success, error); + } + + reject('Template not provided.'); + }); + } + + function logError(error) { + busy = false; + console.error(error); + } + + function escToClose(element) { + var keyup = function (event) { + if(event.keyCode === ESCAPE) { + element.remove(); + } + }; + + body.on('keyup', keyup); + + element.on('$destroy', function () { + body.off('keyup', keyup); + }); + } + + function focusOnOpen(element) { + $mdUtil.nextTick(function () { + var autofocus = $mdUtil.findFocusTarget(element); + + if(autofocus) { + autofocus.focus(); + } + }, false); + } + + function positionDialog(element, target) { + var table = angular.element(target).controller('mdCell').getTable(); + + var getHeight = function () { + return element.prop('clientHeight'); + }; + + var getSize = function () { + return { + width: getWidth(), + height: getHeight() + }; + }; + + var getTableBounds = function () { + var parent = table.parent(); + + if(parent.prop('tagName') === 'MD-TABLE-CONTAINER') { + return parent[0].getBoundingClientRect(); + } else { + return table[0].getBoundingClientRect(); + } + }; + + var getWidth = function () { + return element.prop('clientWidth'); + }; + + var reposition = function () { + var size = getSize(); + var cellBounds = target.getBoundingClientRect(); + var tableBounds = getTableBounds(); + + if(size.width > tableBounds.right - cellBounds.left) { + element.css('left', tableBounds.right - size.width + 'px'); + } else { + element.css('left', cellBounds.left + 'px'); + } + + if(size.height > tableBounds.bottom - cellBounds.top) { + element.css('top', tableBounds.bottom - size.height + 'px'); + } else { + element.css('top', cellBounds.top + 1 + 'px'); + } + + element.css('minWidth', cellBounds.width + 'px'); + }; + + var watchWidth = $rootScope.$watch(getWidth, reposition); + var watchHeight = $rootScope.$watch(getHeight, reposition); + + $window.addEventListener('resize', reposition); + + element.on('$destroy', function () { + watchWidth(); + watchHeight(); + + $window.removeEventListener('resize', reposition); + }); + } + + function preset(size, options) { + + function getAttrs() { + var attrs = 'type="' + (options.type || 'text') + '"'; + + for(var attr in options.validators) { + attrs += ' ' + attr + '="' + options.validators[attr] + '"'; + } + + return attrs; + } + + return { + controller: ['$element', '$q', 'save', '$scope', function ($element, $q, save, $scope) { + function update() { + if($scope.editDialog.$invalid) { + return $q.reject(); + } + + if(angular.isFunction(save)) { + return $q.when(save($scope.editDialog.input)); + } + + return $q.resolve(); + } + + this.dismiss = function () { + $element.remove(); + }; + + this.getInput = function () { + return $scope.editDialog.input; + }; + + $scope.dismiss = this.dismiss; + + $scope.submit = function () { + update().then(function () { + $scope.dismiss(); + }); + }; + }], + locals: { + save: options.save + }, + scope: { + cancel: options.cancel || 'Cancel', + messages: options.messages, + model: options.modelValue, + ok: options.ok || 'Save', + placeholder: options.placeholder, + title: options.title, + size: size + }, + template: + '' + + '
      ' + + '
      {{title || \'Edit\'}}
      ' + + '
      ' + + '' + + '' + + '
      ' + + '
      {{message}}
      ' + + '
      ' + + '
      ' + + '
      ' + + '
      ' + + '
      ' + + '{{cancel}}' + + '{{ok}}' + + '
      ' + + '
      ' + }; + } + + this.show = function (options) { + if(busy) { + return $q.reject(); + } + + busy = true; + options = angular.extend({}, defaultOptions, options); + + if(!options.targetEvent) { + return logError('options.targetEvent is required to align the dialog with the table cell.'); + } + + if(!options.targetEvent.currentTarget.classList.contains('md-cell')) { + return logError('The event target must be a table cell.'); + } + + if(options.bindToController && !options.controllerAs) { + return logError('You must define options.controllerAs when options.bindToController is true.'); + } + + options.target = options.targetEvent.currentTarget; + + var promise = getTemplate(options); + var promises = [promise]; + + for(var prop in options.resolve) { + promise = options.resolve[prop]; + promises.push($q.when(angular.isFunction(promise) ? promise() : promise)); + } + + promise = $q.all(promises); + + promise['catch'](logError); + + return promise.then(function (results) { + var template = results.shift(); + + for(var prop in options.resolve) { + options.resolve[prop] = results.shift(); + } + + return build(template, options); + }); + }; + + this.small = function (options) { + return this.show(angular.extend({}, options, preset('small', options))); + }.bind(this); + + this.large = function (options) { + return this.show(angular.extend({}, options, preset('large', options))); + }.bind(this); + + return this; +} + +mdEditDialog.$inject = ['$compile', '$controller', '$document', '$mdUtil', '$q', '$rootScope', '$templateCache', '$templateRequest', '$window']; + + +angular.module('md.data.table').directive('mdFoot', mdFoot); + +function mdFoot() { + + function compile(tElement) { + tElement.addClass('md-foot'); + } + + return { + compile: compile, + restrict: 'A' + }; +} + +angular.module('md.data.table').directive('mdHead', mdHead); + +function mdHead($compile) { + + function compile(tElement) { + tElement.addClass('md-head'); + return postLink; + } + + // empty controller to be bind scope properties to + function Controller() { + + } + + function postLink(scope, element, attrs, tableCtrl) { + // because scope.$watch is unpredictable + var oldValue = new Array(2); + + function addCheckboxColumn() { + element.children().prepend(''); + } + + function attatchCheckbox() { + element.prop('lastElementChild').firstElementChild.appendChild($compile(createCheckBox())(scope)[0]); + } + + function createCheckBox() { + return angular.element('').attr({ + 'aria-label': 'Select All', + 'ng-click': 'toggleAll()', + 'ng-checked': 'allSelected()', + 'ng-disabled': '!getSelectableRows().length' + }); + } + + function detachCheckbox() { + var cell = element.prop('lastElementChild').firstElementChild; + + if(cell.classList.contains('md-checkbox-column')) { + angular.element(cell).empty(); + } + } + + function enableRowSelection() { + return tableCtrl.$$rowSelect; + } + + function mdSelectCtrl(row) { + return angular.element(row).controller('mdSelect'); + } + + function removeCheckboxColumn() { + Array.prototype.some.call(element.find('th'), function (cell) { + return cell.classList.contains('md-checkbox-column') && cell.remove(); + }); + } + + scope.allSelected = function () { + var rows = scope.getSelectableRows(); + + return rows.length && rows.every(function (row) { + return row.isSelected(); + }); + }; + + scope.getSelectableRows = function () { + return tableCtrl.getBodyRows().map(mdSelectCtrl).filter(function (ctrl) { + return ctrl && !ctrl.disabled; + }); + }; + + scope.selectAll = function () { + tableCtrl.getBodyRows().map(mdSelectCtrl).forEach(function (ctrl) { + if(ctrl && !ctrl.isSelected()) { + ctrl.select(); + } + }); + }; + + scope.toggleAll = function () { + return scope.allSelected() ? scope.unSelectAll() : scope.selectAll(); + }; + + scope.unSelectAll = function () { + tableCtrl.getBodyRows().map(mdSelectCtrl).forEach(function (ctrl) { + if(ctrl && ctrl.isSelected()) { + ctrl.deselect(); + } + }); + }; + + scope.$watchGroup([enableRowSelection, tableCtrl.enableMultiSelect], function (newValue) { + if(newValue[0] !== oldValue[0]) { + if(newValue[0]) { + addCheckboxColumn(); + + if(newValue[1]) { + attatchCheckbox(); + } + } else { + removeCheckboxColumn(); + } + } else if(newValue[0] && newValue[1] !== oldValue[1]) { + if(newValue[1]) { + attatchCheckbox(); + } else { + detachCheckbox(); + } + } + + angular.copy(newValue, oldValue); + }); + } + + return { + bindToController: true, + compile: compile, + controller: Controller, + controllerAs: '$mdHead', + require: '^^mdTable', + restrict: 'A', + scope: { + order: '=?mdOrder', + onReorder: '=?mdOnReorder' + } + }; +} + +mdHead.$inject = ['$compile']; + +angular.module('md.data.table').directive('mdRow', mdRow); + +function mdRow() { + + function compile(tElement) { + tElement.addClass('md-row'); + return postLink; + } + + function postLink(scope, element, attrs, tableCtrl) { + function enableRowSelection() { + return tableCtrl.$$rowSelect; + } + + function isBodyRow() { + return tableCtrl.getBodyRows().indexOf(element[0]) !== -1; + } + + function isChild(node) { + return element[0].contains(node[0]); + } + + if(isBodyRow()) { + var cell = angular.element(''); + + scope.$watch(enableRowSelection, function (enable) { + // if a row is not selectable, prepend an empty cell to it + if(enable && !attrs.mdSelect) { + if(!isChild(cell)) { + element.prepend(cell); + } + return; + } + + if(isChild(cell)) { + cell.remove(); + } + }); + } + } + + return { + compile: compile, + require: '^^mdTable', + restrict: 'A' + }; +} + +angular.module('md.data.table').directive('mdSelect', mdSelect); + +function mdSelect($compile, $parse) { + + // empty controller to bind scope properties to + function Controller() { + + } + + function postLink(scope, element, attrs, ctrls) { + var self = ctrls.shift(); + var tableCtrl = ctrls.shift(); + var getId = $parse(attrs.mdSelectId); + + self.id = getId(self.model); + + if(tableCtrl.$$rowSelect && self.id) { + if(tableCtrl.$$hash.has(self.id)) { + var index = tableCtrl.selected.indexOf(tableCtrl.$$hash.get(self.id)); + + // if the item is no longer selected remove it + if(index === -1) { + tableCtrl.$$hash.purge(self.id); + } + + // if the item is not a reference to the current model update the reference + else if(!tableCtrl.$$hash.equals(self.id, self.model)) { + tableCtrl.$$hash.update(self.id, self.model); + tableCtrl.selected.splice(index, 1, self.model); + } + + } else { + + // check if the item has been selected + tableCtrl.selected.some(function (item, index) { + if(getId(item) === self.id) { + tableCtrl.$$hash.update(self.id, self.model); + tableCtrl.selected.splice(index, 1, self.model); + + return true; + } + }); + } + } + + self.isSelected = function () { + if(!tableCtrl.$$rowSelect) { + return false; + } + + if(self.id) { + return tableCtrl.$$hash.has(self.id); + } + + return tableCtrl.selected.indexOf(self.model) !== -1; + }; + + self.select = function () { + if(self.disabled) { + return; + } + + if(tableCtrl.enableMultiSelect()) { + tableCtrl.selected.push(self.model); + } else { + tableCtrl.selected.splice(0, tableCtrl.selected.length, self.model); + } + + if(angular.isFunction(self.onSelect)) { + self.onSelect(self.model); + } + }; + + self.deselect = function () { + if(self.disabled) { + return; + } + + tableCtrl.selected.splice(tableCtrl.selected.indexOf(self.model), 1); + + if(angular.isFunction(self.onDeselect)) { + self.onDeselect(self.model); + } + }; + + self.toggle = function (event) { + if(event && event.stopPropagation) { + event.stopPropagation(); + } + + return self.isSelected() ? self.deselect() : self.select(); + }; + + function autoSelect() { + return attrs.mdAutoSelect === '' || self.autoSelect; + } + + function createCheckbox() { + var checkbox = angular.element('').attr({ + 'aria-label': 'Select Row', + 'ng-click': '$mdSelect.toggle($event)', + 'ng-checked': '$mdSelect.isSelected()', + 'ng-disabled': '$mdSelect.disabled' + }); + + return angular.element('').append($compile(checkbox)(scope)); + } + + function disableSelection() { + Array.prototype.some.call(element.children(), function (child) { + return child.classList.contains('md-checkbox-cell') && element[0].removeChild(child); + }); + + if(autoSelect()) { + element.off('click', toggle); + } + } + + function enableSelection() { + element.prepend(createCheckbox()); + + if(autoSelect()) { + element.on('click', toggle); + } + } + + function enableRowSelection() { + return tableCtrl.$$rowSelect; + } + + function onSelectChange(selected) { + if(!self.id) { + return; + } + + if(tableCtrl.$$hash.has(self.id)) { + // check if the item has been deselected + if(selected.indexOf(tableCtrl.$$hash.get(self.id)) === -1) { + tableCtrl.$$hash.purge(self.id); + } + + return; + } + + // check if the item has been selected + if(selected.indexOf(self.model) !== -1) { + tableCtrl.$$hash.update(self.id, self.model); + } + } + + function toggle(event) { + scope.$applyAsync(function () { + self.toggle(event); + }); + } + + scope.$watch(enableRowSelection, function (enable) { + if(enable) { + enableSelection(); + } else { + disableSelection(); + } + }); + + scope.$watch(autoSelect, function (newValue, oldValue) { + if(newValue === oldValue) { + return; + } + + if(tableCtrl.$$rowSelect && newValue) { + element.on('click', toggle); + } else { + element.off('click', toggle); + } + }); + + scope.$watch(self.isSelected, function (isSelected) { + return isSelected ? element.addClass('md-selected') : element.removeClass('md-selected'); + }); + + scope.$watch(tableCtrl.enableMultiSelect, function (multiple) { + if(tableCtrl.$$rowSelect && !multiple) { + // remove all but the first selected item + tableCtrl.selected.splice(1); + } + }); + + tableCtrl.registerModelChangeListener(onSelectChange); + + element.on('$destroy', function () { + tableCtrl.removeModelChangeListener(onSelectChange); + }); + } + + return { + bindToController: true, + controller: Controller, + controllerAs: '$mdSelect', + link: postLink, + require: ['mdSelect', '^^mdTable'], + restrict: 'A', + scope: { + model: '=mdSelect', + disabled: '=ngDisabled', + onSelect: '=?mdOnSelect', + onDeselect: '=?mdOnDeselect', + autoSelect: '=mdAutoSelect' + } + }; +} + +mdSelect.$inject = ['$compile', '$parse']; + +angular.module('md.data.table').directive('mdTable', mdTable); + +function Hash() { + var keys = {}; + + this.equals = function (key, item) { + return keys[key] === item; + }; + + this.get = function (key) { + return keys[key]; + }; + + this.has = function (key) { + return keys.hasOwnProperty(key); + }; + + this.purge = function (key) { + delete keys[key]; + }; + + this.update = function (key, item) { + keys[key] = item; + }; +} + +function mdTable() { + + function compile(tElement, tAttrs) { + tElement.addClass('md-table'); + + if(tAttrs.hasOwnProperty('mdProgress')) { + var body = tElement.find('tbody')[0]; + var progress = angular.element(''); + + if(body) { + tElement[0].insertBefore(progress[0], body); + } + } + } + + function Controller($attrs, $element, $q, $scope) { + var self = this; + var queue = []; + var watchListener; + var modelChangeListeners = []; + + self.$$hash = new Hash(); + self.$$columns = {}; + + function enableRowSelection() { + self.$$rowSelect = true; + + watchListener = $scope.$watchCollection('$mdTable.selected', function (selected) { + modelChangeListeners.forEach(function (listener) { + listener(selected); + }); + }); + + $element.addClass('md-row-select'); + } + + function disableRowSelection() { + self.$$rowSelect = false; + + if(angular.isFunction(watchListener)) { + watchListener(); + } + + $element.removeClass('md-row-select'); + } + + function resolvePromises() { + if(!queue.length) { + return $scope.$applyAsync(); + } + + queue[0]['finally'](function () { + queue.shift(); + resolvePromises(); + }); + } + + function rowSelect() { + return $attrs.mdRowSelect === '' || self.rowSelect; + } + + function validateModel() { + if(!self.selected) { + return console.error('Row selection: ngModel is not defined.'); + } + + if(!angular.isArray(self.selected)) { + return console.error('Row selection: Expected an array. Recived ' + typeof self.selected + '.'); + } + + return true; + } + + self.columnCount = function () { + return self.getRows($element[0]).reduce(function (count, row) { + return row.cells.length > count ? row.cells.length : count; + }, 0); + }; + + self.getRows = function (element) { + return Array.prototype.filter.call(element.rows, function (row) { + return !row.classList.contains('ng-leave'); + }); + }; + + self.getBodyRows = function () { + return Array.prototype.reduce.call($element.prop('tBodies'), function (result, tbody) { + return result.concat(self.getRows(tbody)); + }, []); + }; + + self.getElement = function () { + return $element; + }; + + self.getHeaderRows = function () { + return self.getRows($element.prop('tHead')); + }; + + self.enableMultiSelect = function () { + return $attrs.multiple === '' || $scope.$eval($attrs.multiple); + }; + + self.waitingOnPromise = function () { + return !!queue.length; + }; + + self.queuePromise = function (promise) { + if(!promise) { + return; + } + + if(queue.push(angular.isArray(promise) ? $q.all(promise) : $q.when(promise)) === 1) { + resolvePromises(); + } + }; + + self.registerModelChangeListener = function (listener) { + modelChangeListeners.push(listener); + }; + + self.removeModelChangeListener = function (listener) { + var index = modelChangeListeners.indexOf(listener); + + if(index !== -1) { + modelChangeListeners.splice(index, 1); + } + }; + + if($attrs.hasOwnProperty('mdProgress')) { + $scope.$watch('$mdTable.progress', self.queuePromise); + } + + $scope.$watch(rowSelect, function (enable) { + if(enable && !!validateModel()) { + enableRowSelection(); + } else { + disableRowSelection(); + } + }); + } + + Controller.$inject = ['$attrs', '$element', '$q', '$scope']; + + return { + bindToController: true, + compile: compile, + controller: Controller, + controllerAs: '$mdTable', + restrict: 'A', + scope: { + progress: '=?mdProgress', + selected: '=ngModel', + rowSelect: '=mdRowSelect' + } + }; +} + +angular.module('md.data.table').directive('mdTablePagination', mdTablePagination); + +function mdTablePagination() { + + function compile(tElement) { + tElement.addClass('md-table-pagination'); + } + + function Controller($attrs, $mdUtil, $scope) { + var self = this; + var defaultLabel = { + page: 'Page:', + rowsPerPage: 'Rows per page:', + of: 'of' + }; + + self.label = angular.copy(defaultLabel); + + function isPositive(number) { + return parseInt(number, 10) > 0; + } + + self.eval = function (expression) { + return $scope.$eval(expression); + }; + + self.first = function () { + self.page = 1; + self.onPaginationChange(); + }; + + self.hasNext = function () { + return self.page * self.limit < self.total; + }; + + self.hasPrevious = function () { + return self.page > 1; + }; + + self.last = function () { + self.page = self.pages(); + self.onPaginationChange(); + }; + + self.max = function () { + return self.hasNext() ? self.page * self.limit : self.total; + }; + + self.min = function () { + return isPositive(self.total) ? self.page * self.limit - self.limit + 1 : 0; + }; + + self.next = function () { + self.page++; + self.onPaginationChange(); + }; + + self.onPaginationChange = function () { + if(angular.isFunction(self.onPaginate)) { + $mdUtil.nextTick(function () { + self.onPaginate(self.page, self.limit); + }); + } + }; + + self.pages = function () { + return isPositive(self.total) ? Math.ceil(self.total / (isPositive(self.limit) ? self.limit : 1)) : 1; + }; + + self.previous = function () { + self.page--; + self.onPaginationChange(); + }; + + self.showBoundaryLinks = function () { + return $attrs.mdBoundaryLinks === '' || self.boundaryLinks; + }; + + self.showPageSelect = function () { + return $attrs.mdPageSelect === '' || self.pageSelect; + }; + + $scope.$watch('$pagination.limit', function (newValue, oldValue) { + if(isNaN(newValue) || isNaN(oldValue) || newValue === oldValue) { + return; + } + + // find closest page from previous min + self.page = Math.floor(((self.page * oldValue - oldValue) + newValue) / (isPositive(newValue) ? newValue : 1)); + self.onPaginationChange(); + }); + + $attrs.$observe('mdLabel', function (label) { + angular.extend(self.label, defaultLabel, $scope.$eval(label)); + }); + + $scope.$watch('$pagination.total', function (newValue, oldValue) { + if(isNaN(newValue) || newValue === oldValue) { + return; + } + + if(self.page > self.pages()) { + self.last(); + } + }); + } + + Controller.$inject = ['$attrs', '$mdUtil', '$scope']; + + return { + bindToController: { + boundaryLinks: '=?mdBoundaryLinks', + disabled: '=ngDisabled', + limit: '=mdLimit', + page: '=mdPage', + pageSelect: '=?mdPageSelect', + onPaginate: '=?mdOnPaginate', + limitOptions: '=?mdLimitOptions', + total: '@mdTotal' + }, + compile: compile, + controller: Controller, + controllerAs: '$pagination', + restrict: 'E', + scope: {}, + templateUrl: 'md-table-pagination.html' + }; +} + +angular.module('md.data.table').directive('mdTableProgress', mdTableProgress); + +function mdTableProgress() { + + function postLink(scope, element, attrs, tableCtrl) { + scope.columnCount = tableCtrl.columnCount; + scope.deferred = tableCtrl.waitingOnPromise; + } + + return { + link: postLink, + require: '^^mdTable', + restrict: 'A', + scope: {}, + templateUrl: 'md-table-progress.html' + }; +} + +angular.module('md.data.table').directive('virtualPageSelect', virtualPageSelect); + +function virtualPageSelect() { + + function Controller($element, $scope) { + var self = this; + var content = $element.find('md-content'); + + self.pages = []; + + function getMin(pages, total) { + return Math.min(pages, isFinite(total) && isPositive(total) ? total : 1); + } + + function isPositive(number) { + return number > 0; + } + + function setPages(max) { + if(self.pages.length > max) { + return self.pages.splice(max); + } + + for(var i = self.pages.length; i < max; i++) { + self.pages.push(i + 1); + } + } + + content.on('scroll', function () { + if((content.prop('clientHeight') + content.prop('scrollTop')) >= content.prop('scrollHeight')) { + $scope.$applyAsync(function () { + setPages(getMin(self.pages.length + 10, self.total)); + }); + } + }); + + $scope.$watch('$pageSelect.total', function (total) { + setPages(getMin(Math.max(self.pages.length, 10), total)); + }); + + $scope.$watch('$pagination.page', function (page) { + for(var i = self.pages.length; i < page; i++) { + self.pages.push(i + 1); + } + }); + } + + Controller.$inject = ['$element', '$scope']; + + return { + bindToController: { + total: '@' + }, + controller: Controller, + controllerAs: '$pageSelect' + }; +} + +})(window, angular); +/*! + * Angular Material Design + * https://github.com/angular/material + * @license MIT + * v1.1.1 + */ +(function( window, angular, undefined ){ +"use strict"; + +(function(){ +"use strict"; + +angular.module('ngMaterial', ["ng","ngAnimate","ngAria","material.core","material.core.gestures","material.core.layout","material.core.meta","material.core.theming.palette","material.core.theming","material.core.animate","material.components.autocomplete","material.components.backdrop","material.components.bottomSheet","material.components.button","material.components.card","material.components.chips","material.components.checkbox","material.components.colors","material.components.content","material.components.datepicker","material.components.dialog","material.components.divider","material.components.fabActions","material.components.fabShared","material.components.fabSpeedDial","material.components.fabToolbar","material.components.gridList","material.components.icon","material.components.input","material.components.list","material.components.menu","material.components.menuBar","material.components.navBar","material.components.panel","material.components.progressCircular","material.components.progressLinear","material.components.radioButton","material.components.select","material.components.showHide","material.components.sidenav","material.components.slider","material.components.sticky","material.components.subheader","material.components.swipe","material.components.switch","material.components.tabs","material.components.toast","material.components.toolbar","material.components.tooltip","material.components.virtualRepeat","material.components.whiteframe"]); +})(); +(function(){ +"use strict"; + +/** + * Initialization function that validates environment + * requirements. + */ +DetectNgTouch.$inject = ["$log", "$injector"]; +MdCoreConfigure.$inject = ["$provide", "$mdThemingProvider"]; +rAFDecorator.$inject = ["$delegate"]; +angular + .module('material.core', [ + 'ngAnimate', + 'material.core.animate', + 'material.core.layout', + 'material.core.gestures', + 'material.core.theming' + ]) + .config(MdCoreConfigure) + .run(DetectNgTouch); + + +/** + * Detect if the ng-Touch module is also being used. + * Warn if detected. + * @ngInject + */ +function DetectNgTouch($log, $injector) { + if ( $injector.has('$swipe') ) { + var msg = "" + + "You are using the ngTouch module. \n" + + "Angular Material already has mobile click, tap, and swipe support... \n" + + "ngTouch is not supported with Angular Material!"; + $log.warn(msg); + } +} + +/** + * @ngInject + */ +function MdCoreConfigure($provide, $mdThemingProvider) { + + $provide.decorator('$$rAF', ["$delegate", rAFDecorator]); + + $mdThemingProvider.theme('default') + .primaryPalette('indigo') + .accentPalette('pink') + .warnPalette('deep-orange') + .backgroundPalette('grey'); +} + +/** + * @ngInject + */ +function rAFDecorator($delegate) { + /** + * Use this to throttle events that come in often. + * The throttled function will always use the *last* invocation before the + * coming frame. + * + * For example, window resize events that fire many times a second: + * If we set to use an raf-throttled callback on window resize, then + * our callback will only be fired once per frame, with the last resize + * event that happened before that frame. + * + * @param {function} callback function to debounce + */ + $delegate.throttle = function(cb) { + var queuedArgs, alreadyQueued, queueCb, context; + return function debounced() { + queuedArgs = arguments; + context = this; + queueCb = cb; + if (!alreadyQueued) { + alreadyQueued = true; + $delegate(function() { + queueCb.apply(context, Array.prototype.slice.call(queuedArgs)); + alreadyQueued = false; + }); + } + }; + }; + return $delegate; +} + +})(); +(function(){ +"use strict"; + +angular.module('material.core') + .directive('mdAutofocus', MdAutofocusDirective) + + // Support the deprecated md-auto-focus and md-sidenav-focus as well + .directive('mdAutoFocus', MdAutofocusDirective) + .directive('mdSidenavFocus', MdAutofocusDirective); + +/** + * @ngdoc directive + * @name mdAutofocus + * @module material.core.util + * + * @description + * + * `[md-autofocus]` provides an optional way to identify the focused element when a `$mdDialog`, + * `$mdBottomSheet`, `$mdMenu` or `$mdSidenav` opens or upon page load for input-like elements. + * + * When one of these opens, it will find the first nested element with the `[md-autofocus]` + * attribute directive and optional expression. An expression may be specified as the directive + * value to enable conditional activation of the autofocus. + * + * @usage + * + * ### Dialog + * + * + *
      + * + * + * + * + *
      + *
      + *
      + * + * ### Bottomsheet + * + * + * Comment Actions + * + * + * + * + * + * {{ item.name }} + * + * + * + * + * + * + * + * ### Autocomplete + * + * + * {{item.display}} + * + * + * + * ### Sidenav + * + *
      + * + * Left Nav! + * + * + * + * Center Content + * + * Open Left Menu + * + * + * + * + *
      + * + * + * + * + *
      + *
      + *
      + *
      + **/ +function MdAutofocusDirective() { + return { + restrict: 'A', + + link: postLink + } +} + +function postLink(scope, element, attrs) { + var attr = attrs.mdAutoFocus || attrs.mdAutofocus || attrs.mdSidenavFocus; + + // Setup a watcher on the proper attribute to update a class we can check for in $mdUtil + scope.$watch(attr, function(canAutofocus) { + element.toggleClass('md-autofocus', canAutofocus); + }); +} + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.core.colorUtil + * @description + * Color Util + */ +angular + .module('material.core') + .factory('$mdColorUtil', ColorUtilFactory); + +function ColorUtilFactory() { + /** + * Converts hex value to RGBA string + * @param color {string} + * @returns {string} + */ + function hexToRgba (color) { + var hex = color[ 0 ] === '#' ? color.substr(1) : color, + dig = hex.length / 3, + red = hex.substr(0, dig), + green = hex.substr(dig, dig), + blue = hex.substr(dig * 2); + if (dig === 1) { + red += red; + green += green; + blue += blue; + } + return 'rgba(' + parseInt(red, 16) + ',' + parseInt(green, 16) + ',' + parseInt(blue, 16) + ',0.1)'; + } + + /** + * Converts rgba value to hex string + * @param color {string} + * @returns {string} + */ + function rgbaToHex(color) { + color = color.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i); + + var hex = (color && color.length === 4) ? "#" + + ("0" + parseInt(color[1],10).toString(16)).slice(-2) + + ("0" + parseInt(color[2],10).toString(16)).slice(-2) + + ("0" + parseInt(color[3],10).toString(16)).slice(-2) : ''; + + return hex.toUpperCase(); + } + + /** + * Converts an RGB color to RGBA + * @param color {string} + * @returns {string} + */ + function rgbToRgba (color) { + return color.replace(')', ', 0.1)').replace('(', 'a('); + } + + /** + * Converts an RGBA color to RGB + * @param color {string} + * @returns {string} + */ + function rgbaToRgb (color) { + return color + ? color.replace('rgba', 'rgb').replace(/,[^\),]+\)/, ')') + : 'rgb(0,0,0)'; + } + + return { + rgbaToHex: rgbaToHex, + hexToRgba: hexToRgba, + rgbToRgba: rgbToRgba, + rgbaToRgb: rgbaToRgb + } +} +})(); +(function(){ +"use strict"; + + +MdConstantFactory.$inject = ["$sniffer", "$window", "$document"];angular.module('material.core') +.factory('$mdConstant', MdConstantFactory); + +/** + * Factory function that creates the grab-bag $mdConstant service. + * @ngInject + */ +function MdConstantFactory($sniffer, $window, $document) { + + var vendorPrefix = $sniffer.vendorPrefix; + var isWebkit = /webkit/i.test(vendorPrefix); + var SPECIAL_CHARS_REGEXP = /([:\-_]+(.))/g; + var prefixTestEl = document.createElement('div'); + + function vendorProperty(name) { + // Add a dash between the prefix and name, to be able to transform the string into camelcase. + var prefixedName = vendorPrefix + '-' + name; + var ucPrefix = camelCase(prefixedName); + var lcPrefix = ucPrefix.charAt(0).toLowerCase() + ucPrefix.substring(1); + + return hasStyleProperty(name) ? name : // The current browser supports the un-prefixed property + hasStyleProperty(ucPrefix) ? ucPrefix : // The current browser only supports the prefixed property. + hasStyleProperty(lcPrefix) ? lcPrefix : name; // Some browsers are only supporting the prefix in lowercase. + } + + function hasStyleProperty(property) { + return angular.isDefined(prefixTestEl.style[property]); + } + + function camelCase(input) { + return input.replace(SPECIAL_CHARS_REGEXP, function(matches, separator, letter, offset) { + return offset ? letter.toUpperCase() : letter; + }); + } + + var self = { + isInputKey : function(e) { return (e.keyCode >= 31 && e.keyCode <= 90); }, + isNumPadKey : function (e){ return (3 === e.location && e.keyCode >= 97 && e.keyCode <= 105); }, + isNavigationKey : function(e) { + var kc = self.KEY_CODE, NAVIGATION_KEYS = [kc.SPACE, kc.ENTER, kc.UP_ARROW, kc.DOWN_ARROW]; + return (NAVIGATION_KEYS.indexOf(e.keyCode) != -1); + }, + + KEY_CODE: { + COMMA: 188, + SEMICOLON : 186, + ENTER: 13, + ESCAPE: 27, + SPACE: 32, + PAGE_UP: 33, + PAGE_DOWN: 34, + END: 35, + HOME: 36, + LEFT_ARROW : 37, + UP_ARROW : 38, + RIGHT_ARROW : 39, + DOWN_ARROW : 40, + TAB : 9, + BACKSPACE: 8, + DELETE: 46 + }, + CSS: { + /* Constants */ + TRANSITIONEND: 'transitionend' + (isWebkit ? ' webkitTransitionEnd' : ''), + ANIMATIONEND: 'animationend' + (isWebkit ? ' webkitAnimationEnd' : ''), + + TRANSFORM: vendorProperty('transform'), + TRANSFORM_ORIGIN: vendorProperty('transformOrigin'), + TRANSITION: vendorProperty('transition'), + TRANSITION_DURATION: vendorProperty('transitionDuration'), + ANIMATION_PLAY_STATE: vendorProperty('animationPlayState'), + ANIMATION_DURATION: vendorProperty('animationDuration'), + ANIMATION_NAME: vendorProperty('animationName'), + ANIMATION_TIMING: vendorProperty('animationTimingFunction'), + ANIMATION_DIRECTION: vendorProperty('animationDirection') + }, + /** + * As defined in core/style/variables.scss + * + * $layout-breakpoint-xs: 600px !default; + * $layout-breakpoint-sm: 960px !default; + * $layout-breakpoint-md: 1280px !default; + * $layout-breakpoint-lg: 1920px !default; + * + */ + MEDIA: { + 'xs' : '(max-width: 599px)' , + 'gt-xs' : '(min-width: 600px)' , + 'sm' : '(min-width: 600px) and (max-width: 959px)' , + 'gt-sm' : '(min-width: 960px)' , + 'md' : '(min-width: 960px) and (max-width: 1279px)' , + 'gt-md' : '(min-width: 1280px)' , + 'lg' : '(min-width: 1280px) and (max-width: 1919px)', + 'gt-lg' : '(min-width: 1920px)' , + 'xl' : '(min-width: 1920px)' , + 'landscape' : '(orientation: landscape)' , + 'portrait' : '(orientation: portrait)' , + 'print' : 'print' + }, + MEDIA_PRIORITY: [ + 'xl', + 'gt-lg', + 'lg', + 'gt-md', + 'md', + 'gt-sm', + 'sm', + 'gt-xs', + 'xs', + 'landscape', + 'portrait', + 'print' + ] + }; + + return self; +} + +})(); +(function(){ +"use strict"; + + angular + .module('material.core') + .config( ["$provide", function($provide){ + $provide.decorator('$mdUtil', ['$delegate', function ($delegate){ + /** + * Inject the iterator facade to easily support iteration and accessors + * @see iterator below + */ + $delegate.iterator = MdIterator; + + return $delegate; + } + ]); + }]); + + /** + * iterator is a list facade to easily support iteration and accessors + * + * @param items Array list which this iterator will enumerate + * @param reloop Boolean enables iterator to consider the list as an endless reloop + */ + function MdIterator(items, reloop) { + var trueFn = function() { return true; }; + + if (items && !angular.isArray(items)) { + items = Array.prototype.slice.call(items); + } + + reloop = !!reloop; + var _items = items || [ ]; + + // Published API + return { + items: getItems, + count: count, + + inRange: inRange, + contains: contains, + indexOf: indexOf, + itemAt: itemAt, + + findBy: findBy, + + add: add, + remove: remove, + + first: first, + last: last, + next: angular.bind(null, findSubsequentItem, false), + previous: angular.bind(null, findSubsequentItem, true), + + hasPrevious: hasPrevious, + hasNext: hasNext + + }; + + /** + * Publish copy of the enumerable set + * @returns {Array|*} + */ + function getItems() { + return [].concat(_items); + } + + /** + * Determine length of the list + * @returns {Array.length|*|number} + */ + function count() { + return _items.length; + } + + /** + * Is the index specified valid + * @param index + * @returns {Array.length|*|number|boolean} + */ + function inRange(index) { + return _items.length && ( index > -1 ) && (index < _items.length ); + } + + /** + * Can the iterator proceed to the next item in the list; relative to + * the specified item. + * + * @param item + * @returns {Array.length|*|number|boolean} + */ + function hasNext(item) { + return item ? inRange(indexOf(item) + 1) : false; + } + + /** + * Can the iterator proceed to the previous item in the list; relative to + * the specified item. + * + * @param item + * @returns {Array.length|*|number|boolean} + */ + function hasPrevious(item) { + return item ? inRange(indexOf(item) - 1) : false; + } + + /** + * Get item at specified index/position + * @param index + * @returns {*} + */ + function itemAt(index) { + return inRange(index) ? _items[index] : null; + } + + /** + * Find all elements matching the key/value pair + * otherwise return null + * + * @param val + * @param key + * + * @return array + */ + function findBy(key, val) { + return _items.filter(function(item) { + return item[key] === val; + }); + } + + /** + * Add item to list + * @param item + * @param index + * @returns {*} + */ + function add(item, index) { + if ( !item ) return -1; + + if (!angular.isNumber(index)) { + index = _items.length; + } + + _items.splice(index, 0, item); + + return indexOf(item); + } + + /** + * Remove item from list... + * @param item + */ + function remove(item) { + if ( contains(item) ){ + _items.splice(indexOf(item), 1); + } + } + + /** + * Get the zero-based index of the target item + * @param item + * @returns {*} + */ + function indexOf(item) { + return _items.indexOf(item); + } + + /** + * Boolean existence check + * @param item + * @returns {boolean} + */ + function contains(item) { + return item && (indexOf(item) > -1); + } + + /** + * Return first item in the list + * @returns {*} + */ + function first() { + return _items.length ? _items[0] : null; + } + + /** + * Return last item in the list... + * @returns {*} + */ + function last() { + return _items.length ? _items[_items.length - 1] : null; + } + + /** + * Find the next item. If reloop is true and at the end of the list, it will go back to the + * first item. If given, the `validate` callback will be used to determine whether the next item + * is valid. If not valid, it will try to find the next item again. + * + * @param {boolean} backwards Specifies the direction of searching (forwards/backwards) + * @param {*} item The item whose subsequent item we are looking for + * @param {Function=} validate The `validate` function + * @param {integer=} limit The recursion limit + * + * @returns {*} The subsequent item or null + */ + function findSubsequentItem(backwards, item, validate, limit) { + validate = validate || trueFn; + + var curIndex = indexOf(item); + while (true) { + if (!inRange(curIndex)) return null; + + var nextIndex = curIndex + (backwards ? -1 : 1); + var foundItem = null; + if (inRange(nextIndex)) { + foundItem = _items[nextIndex]; + } else if (reloop) { + foundItem = backwards ? last() : first(); + nextIndex = indexOf(foundItem); + } + + if ((foundItem === null) || (nextIndex === limit)) return null; + if (validate(foundItem)) return foundItem; + + if (angular.isUndefined(limit)) limit = nextIndex; + + curIndex = nextIndex; + } + } + } + + +})(); +(function(){ +"use strict"; + + +mdMediaFactory.$inject = ["$mdConstant", "$rootScope", "$window"];angular.module('material.core') +.factory('$mdMedia', mdMediaFactory); + +/** + * @ngdoc service + * @name $mdMedia + * @module material.core + * + * @description + * `$mdMedia` is used to evaluate whether a given media query is true or false given the + * current device's screen / window size. The media query will be re-evaluated on resize, allowing + * you to register a watch. + * + * `$mdMedia` also has pre-programmed support for media queries that match the layout breakpoints: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
      BreakpointmediaQuery
      xs(max-width: 599px)
      gt-xs(min-width: 600px)
      sm(min-width: 600px) and (max-width: 959px)
      gt-sm(min-width: 960px)
      md(min-width: 960px) and (max-width: 1279px)
      gt-md(min-width: 1280px)
      lg(min-width: 1280px) and (max-width: 1919px)
      gt-lg(min-width: 1920px)
      xl(min-width: 1920px)
      landscapelandscape
      portraitportrait
      printprint
      + * + * See Material Design's
      Layout - Adaptive UI for more details. + * + * + * + * + * + * @returns {boolean} a boolean representing whether or not the given media query is true or false. + * + * @usage + * + * app.controller('MyController', function($mdMedia, $scope) { + * $scope.$watch(function() { return $mdMedia('lg'); }, function(big) { + * $scope.bigScreen = big; + * }); + * + * $scope.screenIsSmall = $mdMedia('sm'); + * $scope.customQuery = $mdMedia('(min-width: 1234px)'); + * $scope.anotherCustom = $mdMedia('max-width: 300px'); + * }); + * + */ + +/* @ngInject */ +function mdMediaFactory($mdConstant, $rootScope, $window) { + var queries = {}; + var mqls = {}; + var results = {}; + var normalizeCache = {}; + + $mdMedia.getResponsiveAttribute = getResponsiveAttribute; + $mdMedia.getQuery = getQuery; + $mdMedia.watchResponsiveAttributes = watchResponsiveAttributes; + + return $mdMedia; + + function $mdMedia(query) { + var validated = queries[query]; + if (angular.isUndefined(validated)) { + validated = queries[query] = validate(query); + } + + var result = results[validated]; + if (angular.isUndefined(result)) { + result = add(validated); + } + + return result; + } + + function validate(query) { + return $mdConstant.MEDIA[query] || + ((query.charAt(0) !== '(') ? ('(' + query + ')') : query); + } + + function add(query) { + var result = mqls[query]; + if ( !result ) { + result = mqls[query] = $window.matchMedia(query); + } + + result.addListener(onQueryChange); + return (results[result.media] = !!result.matches); + } + + function onQueryChange(query) { + $rootScope.$evalAsync(function() { + results[query.media] = !!query.matches; + }); + } + + function getQuery(name) { + return mqls[name]; + } + + function getResponsiveAttribute(attrs, attrName) { + for (var i = 0; i < $mdConstant.MEDIA_PRIORITY.length; i++) { + var mediaName = $mdConstant.MEDIA_PRIORITY[i]; + if (!mqls[queries[mediaName]].matches) { + continue; + } + + var normalizedName = getNormalizedName(attrs, attrName + '-' + mediaName); + if (attrs[normalizedName]) { + return attrs[normalizedName]; + } + } + + // fallback on unprefixed + return attrs[getNormalizedName(attrs, attrName)]; + } + + function watchResponsiveAttributes(attrNames, attrs, watchFn) { + var unwatchFns = []; + attrNames.forEach(function(attrName) { + var normalizedName = getNormalizedName(attrs, attrName); + if (angular.isDefined(attrs[normalizedName])) { + unwatchFns.push( + attrs.$observe(normalizedName, angular.bind(void 0, watchFn, null))); + } + + for (var mediaName in $mdConstant.MEDIA) { + normalizedName = getNormalizedName(attrs, attrName + '-' + mediaName); + if (angular.isDefined(attrs[normalizedName])) { + unwatchFns.push( + attrs.$observe(normalizedName, angular.bind(void 0, watchFn, mediaName))); + } + } + }); + + return function unwatch() { + unwatchFns.forEach(function(fn) { fn(); }) + }; + } + + // Improves performance dramatically + function getNormalizedName(attrs, attrName) { + return normalizeCache[attrName] || + (normalizeCache[attrName] = attrs.$normalize(attrName)); + } +} + +})(); +(function(){ +"use strict"; + +angular + .module('material.core') + .config( ["$provide", function($provide) { + $provide.decorator('$mdUtil', ['$delegate', function ($delegate) { + + // Inject the prefixer into our original $mdUtil service. + $delegate.prefixer = MdPrefixer; + + return $delegate; + }]); + }]); + +function MdPrefixer(initialAttributes, buildSelector) { + var PREFIXES = ['data', 'x']; + + if (initialAttributes) { + // The prefixer also accepts attributes as a parameter, and immediately builds a list or selector for + // the specified attributes. + return buildSelector ? _buildSelector(initialAttributes) : _buildList(initialAttributes); + } + + return { + buildList: _buildList, + buildSelector: _buildSelector, + hasAttribute: _hasAttribute, + removeAttribute: _removeAttribute + }; + + function _buildList(attributes) { + attributes = angular.isArray(attributes) ? attributes : [attributes]; + + attributes.forEach(function(item) { + PREFIXES.forEach(function(prefix) { + attributes.push(prefix + '-' + item); + }); + }); + + return attributes; + } + + function _buildSelector(attributes) { + attributes = angular.isArray(attributes) ? attributes : [attributes]; + + return _buildList(attributes) + .map(function(item) { + return '[' + item + ']' + }) + .join(','); + } + + function _hasAttribute(element, attribute) { + element = _getNativeElement(element); + + if (!element) { + return false; + } + + var prefixedAttrs = _buildList(attribute); + + for (var i = 0; i < prefixedAttrs.length; i++) { + if (element.hasAttribute(prefixedAttrs[i])) { + return true; + } + } + + return false; + } + + function _removeAttribute(element, attribute) { + element = _getNativeElement(element); + + if (!element) { + return; + } + + _buildList(attribute).forEach(function(prefixedAttribute) { + element.removeAttribute(prefixedAttribute); + }); + } + + /** + * Transforms a jqLite or DOM element into a HTML element. + * This is useful when supporting jqLite elements and DOM elements at + * same time. + * @param element {JQLite|Element} Element to be parsed + * @returns {HTMLElement} Parsed HTMLElement + */ + function _getNativeElement(element) { + element = element[0] || element; + + if (element.nodeType) { + return element; + } + } + +} +})(); +(function(){ +"use strict"; + +/* + * This var has to be outside the angular factory, otherwise when + * there are multiple material apps on the same page, each app + * will create its own instance of this array and the app's IDs + * will not be unique. + */ +UtilFactory.$inject = ["$document", "$timeout", "$compile", "$rootScope", "$$mdAnimate", "$interpolate", "$log", "$rootElement", "$window", "$$rAF"]; +var nextUniqueId = 0; + +/** + * @ngdoc module + * @name material.core.util + * @description + * Util + */ +angular + .module('material.core') + .factory('$mdUtil', UtilFactory); + +/** + * @ngInject + */ +function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $interpolate, $log, $rootElement, $window, $$rAF) { + // Setup some core variables for the processTemplate method + var startSymbol = $interpolate.startSymbol(), + endSymbol = $interpolate.endSymbol(), + usesStandardSymbols = ((startSymbol === '{{') && (endSymbol === '}}')); + + /** + * Checks if the target element has the requested style by key + * @param {DOMElement|JQLite} target Target element + * @param {string} key Style key + * @param {string=} expectedVal Optional expected value + * @returns {boolean} Whether the target element has the style or not + */ + var hasComputedStyle = function (target, key, expectedVal) { + var hasValue = false; + + if ( target && target.length ) { + var computedStyles = $window.getComputedStyle(target[0]); + hasValue = angular.isDefined(computedStyles[key]) && (expectedVal ? computedStyles[key] == expectedVal : true); + } + + return hasValue; + }; + + function validateCssValue(value) { + return !value ? '0' : + hasPx(value) || hasPercent(value) ? value : value + 'px'; + } + + function hasPx(value) { + return String(value).indexOf('px') > -1; + } + + function hasPercent(value) { + return String(value).indexOf('%') > -1; + + } + + var $mdUtil = { + dom: {}, + now: window.performance ? + angular.bind(window.performance, window.performance.now) : Date.now || function() { + return new Date().getTime(); + }, + + /** + * Bi-directional accessor/mutator used to easily update an element's + * property based on the current 'dir'ectional value. + */ + bidi : function(element, property, lValue, rValue) { + var ltr = !($document[0].dir == 'rtl' || $document[0].body.dir == 'rtl'); + + // If accessor + if ( arguments.length == 0 ) return ltr ? 'ltr' : 'rtl'; + + // If mutator + var elem = angular.element(element); + + if ( ltr && angular.isDefined(lValue)) { + elem.css(property, validateCssValue(lValue)); + } + else if ( !ltr && angular.isDefined(rValue)) { + elem.css(property, validateCssValue(rValue) ); + } + }, + + bidiProperty: function (element, lProperty, rProperty, value) { + var ltr = !($document[0].dir == 'rtl' || $document[0].body.dir == 'rtl'); + + var elem = angular.element(element); + + if ( ltr && angular.isDefined(lProperty)) { + elem.css(lProperty, validateCssValue(value)); + elem.css(rProperty, ''); + } + else if ( !ltr && angular.isDefined(rProperty)) { + elem.css(rProperty, validateCssValue(value) ); + elem.css(lProperty, ''); + } + }, + + clientRect: function(element, offsetParent, isOffsetRect) { + var node = getNode(element); + offsetParent = getNode(offsetParent || node.offsetParent || document.body); + var nodeRect = node.getBoundingClientRect(); + + // The user can ask for an offsetRect: a rect relative to the offsetParent, + // or a clientRect: a rect relative to the page + var offsetRect = isOffsetRect ? + offsetParent.getBoundingClientRect() : + {left: 0, top: 0, width: 0, height: 0}; + return { + left: nodeRect.left - offsetRect.left, + top: nodeRect.top - offsetRect.top, + width: nodeRect.width, + height: nodeRect.height + }; + }, + offsetRect: function(element, offsetParent) { + return $mdUtil.clientRect(element, offsetParent, true); + }, + + // Annoying method to copy nodes to an array, thanks to IE + nodesToArray: function(nodes) { + nodes = nodes || []; + + var results = []; + for (var i = 0; i < nodes.length; ++i) { + results.push(nodes.item(i)); + } + return results; + }, + + /** + * Calculate the positive scroll offset + * TODO: Check with pinch-zoom in IE/Chrome; + * https://code.google.com/p/chromium/issues/detail?id=496285 + */ + scrollTop: function(element) { + element = angular.element(element || $document[0].body); + + var body = (element[0] == $document[0].body) ? $document[0].body : undefined; + var scrollTop = body ? body.scrollTop + body.parentElement.scrollTop : 0; + + // Calculate the positive scroll offset + return scrollTop || Math.abs(element[0].getBoundingClientRect().top); + }, + + /** + * Finds the proper focus target by searching the DOM. + * + * @param containerEl + * @param attributeVal + * @returns {*} + */ + findFocusTarget: function(containerEl, attributeVal) { + var AUTO_FOCUS = this.prefixer('md-autofocus', true); + var elToFocus; + + elToFocus = scanForFocusable(containerEl, attributeVal || AUTO_FOCUS); + + if ( !elToFocus && attributeVal != AUTO_FOCUS) { + // Scan for deprecated attribute + elToFocus = scanForFocusable(containerEl, this.prefixer('md-auto-focus', true)); + + if ( !elToFocus ) { + // Scan for fallback to 'universal' API + elToFocus = scanForFocusable(containerEl, AUTO_FOCUS); + } + } + + return elToFocus; + + /** + * Can target and nested children for specified Selector (attribute) + * whose value may be an expression that evaluates to True/False. + */ + function scanForFocusable(target, selector) { + var elFound, items = target[0].querySelectorAll(selector); + + // Find the last child element with the focus attribute + if ( items && items.length ){ + items.length && angular.forEach(items, function(it) { + it = angular.element(it); + + // Check the element for the md-autofocus class to ensure any associated expression + // evaluated to true. + var isFocusable = it.hasClass('md-autofocus'); + if (isFocusable) elFound = it; + }); + } + return elFound; + } + }, + + /** + * Disables scroll around the passed parent element. + * @param element Unused + * @param {!Element|!angular.JQLite} parent Element to disable scrolling within. + * Defaults to body if none supplied. + * @param options Object of options to modify functionality + * - disableScrollMask Boolean of whether or not to create a scroll mask element or + * use the passed parent element. + */ + disableScrollAround: function(element, parent, options) { + $mdUtil.disableScrollAround._count = $mdUtil.disableScrollAround._count || 0; + ++$mdUtil.disableScrollAround._count; + if ($mdUtil.disableScrollAround._enableScrolling) return $mdUtil.disableScrollAround._enableScrolling; + var body = $document[0].body, + restoreBody = disableBodyScroll(), + restoreElement = disableElementScroll(parent); + + return $mdUtil.disableScrollAround._enableScrolling = function() { + if (!--$mdUtil.disableScrollAround._count) { + restoreBody(); + restoreElement(); + delete $mdUtil.disableScrollAround._enableScrolling; + } + }; + + // Creates a virtual scrolling mask to absorb touchmove, keyboard, scrollbar clicking, and wheel events + function disableElementScroll(element) { + element = angular.element(element || body); + var scrollMask; + if (options && options.disableScrollMask) { + scrollMask = element; + } else { + element = element[0]; + scrollMask = angular.element( + '
      ' + + '
      ' + + '
      '); + element.appendChild(scrollMask[0]); + } + + scrollMask.on('wheel', preventDefault); + scrollMask.on('touchmove', preventDefault); + + return function restoreScroll() { + scrollMask.off('wheel'); + scrollMask.off('touchmove'); + scrollMask[0].parentNode.removeChild(scrollMask[0]); + delete $mdUtil.disableScrollAround._enableScrolling; + }; + + function preventDefault(e) { + e.preventDefault(); + } + } + + // Converts the body to a position fixed block and translate it to the proper scroll position + function disableBodyScroll() { + var htmlNode = body.parentNode; + var restoreHtmlStyle = htmlNode.style.cssText || ''; + var restoreBodyStyle = body.style.cssText || ''; + var scrollOffset = $mdUtil.scrollTop(body); + var clientWidth = body.clientWidth; + + if (body.scrollHeight > body.clientHeight + 1) { + applyStyles(body, { + position: 'fixed', + width: '100%', + top: -scrollOffset + 'px' + }); + + htmlNode.style.overflowY = 'scroll'; + } + + if (body.clientWidth < clientWidth) applyStyles(body, {overflow: 'hidden'}); + + return function restoreScroll() { + body.style.cssText = restoreBodyStyle; + htmlNode.style.cssText = restoreHtmlStyle; + body.scrollTop = scrollOffset; + htmlNode.scrollTop = scrollOffset; + }; + } + + function applyStyles(el, styles) { + for (var key in styles) { + el.style[key] = styles[key]; + } + } + }, + enableScrolling: function() { + var method = this.disableScrollAround._enableScrolling; + method && method(); + }, + floatingScrollbars: function() { + if (this.floatingScrollbars.cached === undefined) { + var tempNode = angular.element('
      ').css({ + width: '100%', + 'z-index': -1, + position: 'absolute', + height: '35px', + 'overflow-y': 'scroll' + }); + tempNode.children().css('height', '60px'); + + $document[0].body.appendChild(tempNode[0]); + this.floatingScrollbars.cached = (tempNode[0].offsetWidth == tempNode[0].childNodes[0].offsetWidth); + tempNode.remove(); + } + return this.floatingScrollbars.cached; + }, + + // Mobile safari only allows you to set focus in click event listeners... + forceFocus: function(element) { + var node = element[0] || element; + + document.addEventListener('click', function focusOnClick(ev) { + if (ev.target === node && ev.$focus) { + node.focus(); + ev.stopImmediatePropagation(); + ev.preventDefault(); + node.removeEventListener('click', focusOnClick); + } + }, true); + + var newEvent = document.createEvent('MouseEvents'); + newEvent.initMouseEvent('click', false, true, window, {}, 0, 0, 0, 0, + false, false, false, false, 0, null); + newEvent.$material = true; + newEvent.$focus = true; + node.dispatchEvent(newEvent); + }, + + /** + * facade to build md-backdrop element with desired styles + * NOTE: Use $compile to trigger backdrop postLink function + */ + createBackdrop: function(scope, addClass) { + return $compile($mdUtil.supplant('', [addClass]))(scope); + }, + + /** + * supplant() method from Crockford's `Remedial Javascript` + * Equivalent to use of $interpolate; without dependency on + * interpolation symbols and scope. Note: the '{}' can + * be property names, property chains, or array indices. + */ + supplant: function(template, values, pattern) { + pattern = pattern || /\{([^\{\}]*)\}/g; + return template.replace(pattern, function(a, b) { + var p = b.split('.'), + r = values; + try { + for (var s in p) { + if (p.hasOwnProperty(s) ) { + r = r[p[s]]; + } + } + } catch (e) { + r = a; + } + return (typeof r === 'string' || typeof r === 'number') ? r : a; + }); + }, + + fakeNgModel: function() { + return { + $fake: true, + $setTouched: angular.noop, + $setViewValue: function(value) { + this.$viewValue = value; + this.$render(value); + this.$viewChangeListeners.forEach(function(cb) { + cb(); + }); + }, + $isEmpty: function(value) { + return ('' + value).length === 0; + }, + $parsers: [], + $formatters: [], + $viewChangeListeners: [], + $render: angular.noop + }; + }, + + // Returns a function, that, as long as it continues to be invoked, will not + // be triggered. The function will be called after it stops being called for + // N milliseconds. + // @param wait Integer value of msecs to delay (since last debounce reset); default value 10 msecs + // @param invokeApply should the $timeout trigger $digest() dirty checking + debounce: function(func, wait, scope, invokeApply) { + var timer; + + return function debounced() { + var context = scope, + args = Array.prototype.slice.call(arguments); + + $timeout.cancel(timer); + timer = $timeout(function() { + + timer = undefined; + func.apply(context, args); + + }, wait || 10, invokeApply); + }; + }, + + // Returns a function that can only be triggered every `delay` milliseconds. + // In other words, the function will not be called unless it has been more + // than `delay` milliseconds since the last call. + throttle: function throttle(func, delay) { + var recent; + return function throttled() { + var context = this; + var args = arguments; + var now = $mdUtil.now(); + + if (!recent || (now - recent > delay)) { + func.apply(context, args); + recent = now; + } + }; + }, + + /** + * Measures the number of milliseconds taken to run the provided callback + * function. Uses a high-precision timer if available. + */ + time: function time(cb) { + var start = $mdUtil.now(); + cb(); + return $mdUtil.now() - start; + }, + + /** + * Create an implicit getter that caches its `getter()` + * lookup value + */ + valueOnUse : function (scope, key, getter) { + var value = null, args = Array.prototype.slice.call(arguments); + var params = (args.length > 3) ? args.slice(3) : [ ]; + + Object.defineProperty(scope, key, { + get: function () { + if (value === null) value = getter.apply(scope, params); + return value; + } + }); + }, + + /** + * Get a unique ID. + * + * @returns {string} an unique numeric string + */ + nextUid: function() { + return '' + nextUniqueId++; + }, + + // Stop watchers and events from firing on a scope without destroying it, + // by disconnecting it from its parent and its siblings' linked lists. + disconnectScope: function disconnectScope(scope) { + if (!scope) return; + + // we can't destroy the root scope or a scope that has been already destroyed + if (scope.$root === scope) return; + if (scope.$$destroyed) return; + + var parent = scope.$parent; + scope.$$disconnected = true; + + // See Scope.$destroy + if (parent.$$childHead === scope) parent.$$childHead = scope.$$nextSibling; + if (parent.$$childTail === scope) parent.$$childTail = scope.$$prevSibling; + if (scope.$$prevSibling) scope.$$prevSibling.$$nextSibling = scope.$$nextSibling; + if (scope.$$nextSibling) scope.$$nextSibling.$$prevSibling = scope.$$prevSibling; + + scope.$$nextSibling = scope.$$prevSibling = null; + + }, + + // Undo the effects of disconnectScope above. + reconnectScope: function reconnectScope(scope) { + if (!scope) return; + + // we can't disconnect the root node or scope already disconnected + if (scope.$root === scope) return; + if (!scope.$$disconnected) return; + + var child = scope; + + var parent = child.$parent; + child.$$disconnected = false; + // See Scope.$new for this logic... + child.$$prevSibling = parent.$$childTail; + if (parent.$$childHead) { + parent.$$childTail.$$nextSibling = child; + parent.$$childTail = child; + } else { + parent.$$childHead = parent.$$childTail = child; + } + }, + + /* + * getClosest replicates jQuery.closest() to walk up the DOM tree until it finds a matching nodeName + * + * @param el Element to start walking the DOM from + * @param check Either a string or a function. If a string is passed, it will be evaluated against + * each of the parent nodes' tag name. If a function is passed, the loop will call it with each of + * the parents and will use the return value to determine whether the node is a match. + * @param onlyParent Only start checking from the parent element, not `el`. + */ + getClosest: function getClosest(el, validateWith, onlyParent) { + if ( angular.isString(validateWith) ) { + var tagName = validateWith.toUpperCase(); + validateWith = function(el) { + return el.nodeName === tagName; + }; + } + + if (el instanceof angular.element) el = el[0]; + if (onlyParent) el = el.parentNode; + if (!el) return null; + + do { + if (validateWith(el)) { + return el; + } + } while (el = el.parentNode); + + return null; + }, + + /** + * Build polyfill for the Node.contains feature (if needed) + */ + elementContains: function(node, child) { + var hasContains = (window.Node && window.Node.prototype && Node.prototype.contains); + var findFn = hasContains ? angular.bind(node, node.contains) : angular.bind(node, function(arg) { + // compares the positions of two nodes and returns a bitmask + return (node === child) || !!(this.compareDocumentPosition(arg) & 16) + }); + + return findFn(child); + }, + + /** + * Functional equivalent for $element.filter(‘md-bottom-sheet’) + * useful with interimElements where the element and its container are important... + * + * @param {[]} elements to scan + * @param {string} name of node to find (e.g. 'md-dialog') + * @param {boolean=} optional flag to allow deep scans; defaults to 'false'. + * @param {boolean=} optional flag to enable log warnings; defaults to false + */ + extractElementByName: function(element, nodeName, scanDeep, warnNotFound) { + var found = scanTree(element); + if (!found && !!warnNotFound) { + $log.warn( $mdUtil.supplant("Unable to find node '{0}' in element '{1}'.",[nodeName, element[0].outerHTML]) ); + } + + return angular.element(found || element); + + /** + * Breadth-First tree scan for element with matching `nodeName` + */ + function scanTree(element) { + return scanLevel(element) || (!!scanDeep ? scanChildren(element) : null); + } + + /** + * Case-insensitive scan of current elements only (do not descend). + */ + function scanLevel(element) { + if ( element ) { + for (var i = 0, len = element.length; i < len; i++) { + if (element[i].nodeName.toLowerCase() === nodeName) { + return element[i]; + } + } + } + return null; + } + + /** + * Scan children of specified node + */ + function scanChildren(element) { + var found; + if ( element ) { + for (var i = 0, len = element.length; i < len; i++) { + var target = element[i]; + if ( !found ) { + for (var j = 0, numChild = target.childNodes.length; j < numChild; j++) { + found = found || scanTree([target.childNodes[j]]); + } + } + } + } + return found; + } + + }, + + /** + * Give optional properties with no value a boolean true if attr provided or false otherwise + */ + initOptionalProperties: function(scope, attr, defaults) { + defaults = defaults || {}; + angular.forEach(scope.$$isolateBindings, function(binding, key) { + if (binding.optional && angular.isUndefined(scope[key])) { + var attrIsDefined = angular.isDefined(attr[binding.attrName]); + scope[key] = angular.isDefined(defaults[key]) ? defaults[key] : attrIsDefined; + } + }); + }, + + /** + * Alternative to $timeout calls with 0 delay. + * nextTick() coalesces all calls within a single frame + * to minimize $digest thrashing + * + * @param callback + * @param digest + * @returns {*} + */ + nextTick: function(callback, digest, scope) { + //-- grab function reference for storing state details + var nextTick = $mdUtil.nextTick; + var timeout = nextTick.timeout; + var queue = nextTick.queue || []; + + //-- add callback to the queue + queue.push({scope: scope, callback: callback}); + + //-- set default value for digest + if (digest == null) digest = true; + + //-- store updated digest/queue values + nextTick.digest = nextTick.digest || digest; + nextTick.queue = queue; + + //-- either return existing timeout or create a new one + return timeout || (nextTick.timeout = $timeout(processQueue, 0, false)); + + /** + * Grab a copy of the current queue + * Clear the queue for future use + * Process the existing queue + * Trigger digest if necessary + */ + function processQueue() { + var queue = nextTick.queue; + var digest = nextTick.digest; + + nextTick.queue = []; + nextTick.timeout = null; + nextTick.digest = false; + + queue.forEach(function(queueItem) { + var skip = queueItem.scope && queueItem.scope.$$destroyed; + if (!skip) { + queueItem.callback(); + } + }); + + if (digest) $rootScope.$digest(); + } + }, + + /** + * Processes a template and replaces the start/end symbols if the application has + * overriden them. + * + * @param template The template to process whose start/end tags may be replaced. + * @returns {*} + */ + processTemplate: function(template) { + if (usesStandardSymbols) { + return template; + } else { + if (!template || !angular.isString(template)) return template; + return template.replace(/\{\{/g, startSymbol).replace(/}}/g, endSymbol); + } + }, + + /** + * Scan up dom hierarchy for enabled parent; + */ + getParentWithPointerEvents: function (element) { + var parent = element.parent(); + + // jqLite might return a non-null, but still empty, parent; so check for parent and length + while (hasComputedStyle(parent, 'pointer-events', 'none')) { + parent = parent.parent(); + } + + return parent; + }, + + getNearestContentElement: function (element) { + var current = element.parent()[0]; + // Look for the nearest parent md-content, stopping at the rootElement. + while (current && current !== $rootElement[0] && current !== document.body && current.nodeName.toUpperCase() !== 'MD-CONTENT') { + current = current.parentNode; + } + return current; + }, + + /** + * Checks if the current browser is natively supporting the `sticky` position. + * @returns {string} supported sticky property name + */ + checkStickySupport: function() { + var stickyProp; + var testEl = angular.element('
      '); + $document[0].body.appendChild(testEl[0]); + + var stickyProps = ['sticky', '-webkit-sticky']; + for (var i = 0; i < stickyProps.length; ++i) { + testEl.css({ + position: stickyProps[i], + top: 0, + 'z-index': 2 + }); + + if (testEl.css('position') == stickyProps[i]) { + stickyProp = stickyProps[i]; + break; + } + } + + testEl.remove(); + + return stickyProp; + }, + + /** + * Parses an attribute value, mostly a string. + * By default checks for negated values and returns `false´ if present. + * Negated values are: (native falsy) and negative strings like: + * `false` or `0`. + * @param value Attribute value which should be parsed. + * @param negatedCheck When set to false, won't check for negated values. + * @returns {boolean} + */ + parseAttributeBoolean: function(value, negatedCheck) { + return value === '' || !!value && (negatedCheck === false || value !== 'false' && value !== '0'); + }, + + hasComputedStyle: hasComputedStyle, + + /** + * Returns true if the parent form of the element has been submitted. + * + * @param element An Angular or HTML5 element. + * + * @returns {boolean} + */ + isParentFormSubmitted: function(element) { + var parent = $mdUtil.getClosest(element, 'form'); + var form = parent ? angular.element(parent).controller('form') : null; + + return form ? form.$submitted : false; + }, + + /** + * Animate the requested element's scrollTop to the requested scrollPosition with basic easing. + * + * @param element The element to scroll. + * @param scrollEnd The new/final scroll position. + */ + animateScrollTo: function(element, scrollEnd) { + var scrollStart = element.scrollTop; + var scrollChange = scrollEnd - scrollStart; + var scrollingDown = scrollStart < scrollEnd; + var startTime = $mdUtil.now(); + + $$rAF(scrollChunk); + + function scrollChunk() { + var newPosition = calculateNewPosition(); + + element.scrollTop = newPosition; + + if (scrollingDown ? newPosition < scrollEnd : newPosition > scrollEnd) { + $$rAF(scrollChunk); + } + } + + function calculateNewPosition() { + var duration = 1000; + var currentTime = $mdUtil.now() - startTime; + + return ease(currentTime, scrollStart, scrollChange, duration); + } + + function ease(currentTime, start, change, duration) { + // If the duration has passed (which can occur if our app loses focus due to $$rAF), jump + // straight to the proper position + if (currentTime > duration) { + return start + change; + } + + var ts = (currentTime /= duration) * currentTime; + var tc = ts * currentTime; + + return start + change * (-2 * tc + 3 * ts); + } + } + }; + + +// Instantiate other namespace utility methods + + $mdUtil.dom.animator = $$mdAnimate($mdUtil); + + return $mdUtil; + + function getNode(el) { + return el[0] || el; + } + +} + +/* + * Since removing jQuery from the demos, some code that uses `element.focus()` is broken. + * We need to add `element.focus()`, because it's testable unlike `element[0].focus`. + */ + +angular.element.prototype.focus = angular.element.prototype.focus || function() { + if (this.length) { + this[0].focus(); + } + return this; + }; +angular.element.prototype.blur = angular.element.prototype.blur || function() { + if (this.length) { + this[0].blur(); + } + return this; + }; + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.core.aria + * @description + * Aria Expectations for ngMaterial components. + */ +MdAriaService.$inject = ["$$rAF", "$log", "$window", "$interpolate"]; +angular + .module('material.core') + .provider('$mdAria', MdAriaProvider); + +/** + * @ngdoc service + * @name $mdAriaProvider + * @module material.core.aria + * + * @description + * + * Modify options of the `$mdAria` service, which will be used by most of the Angular Material + * components. + * + * You are able to disable `$mdAria` warnings, by using the following markup. + * + * + * app.config(function($mdAriaProvider) { + * // Globally disables all ARIA warnings. + * $mdAriaProvider.disableWarnings(); + * }); + * + * + */ +function MdAriaProvider() { + + var self = this; + + /** + * Whether we should show ARIA warnings in the console, if labels are missing on the element + * By default the warnings are enabled + */ + self.showWarnings = true; + + return { + disableWarnings: disableWarnings, + $get: ["$$rAF", "$log", "$window", "$interpolate", function($$rAF, $log, $window, $interpolate) { + return MdAriaService.apply(self, arguments); + }] + }; + + /** + * @ngdoc method + * @name $mdAriaProvider#disableWarnings + * @description Disables all ARIA warnings generated by Angular Material. + */ + function disableWarnings() { + self.showWarnings = false; + } +} + +/* + * @ngInject + */ +function MdAriaService($$rAF, $log, $window, $interpolate) { + + // Load the showWarnings option from the current context and store it inside of a scope variable, + // because the context will be probably lost in some function calls. + var showWarnings = this.showWarnings; + + return { + expect: expect, + expectAsync: expectAsync, + expectWithText: expectWithText, + expectWithoutText: expectWithoutText + }; + + /** + * Check if expected attribute has been specified on the target element or child + * @param element + * @param attrName + * @param {optional} defaultValue What to set the attr to if no value is found + */ + function expect(element, attrName, defaultValue) { + + var node = angular.element(element)[0] || element; + + // if node exists and neither it nor its children have the attribute + if (node && + ((!node.hasAttribute(attrName) || + node.getAttribute(attrName).length === 0) && + !childHasAttribute(node, attrName))) { + + defaultValue = angular.isString(defaultValue) ? defaultValue.trim() : ''; + if (defaultValue.length) { + element.attr(attrName, defaultValue); + } else if (showWarnings) { + $log.warn('ARIA: Attribute "', attrName, '", required for accessibility, is missing on node:', node); + } + + } + } + + function expectAsync(element, attrName, defaultValueGetter) { + // Problem: when retrieving the element's contents synchronously to find the label, + // the text may not be defined yet in the case of a binding. + // There is a higher chance that a binding will be defined if we wait one frame. + $$rAF(function() { + expect(element, attrName, defaultValueGetter()); + }); + } + + function expectWithText(element, attrName) { + var content = getText(element) || ""; + var hasBinding = content.indexOf($interpolate.startSymbol()) > -1; + + if ( hasBinding ) { + expectAsync(element, attrName, function() { + return getText(element); + }); + } else { + expect(element, attrName, content); + } + } + + function expectWithoutText(element, attrName) { + var content = getText(element); + var hasBinding = content.indexOf($interpolate.startSymbol()) > -1; + + if ( !hasBinding && !content) { + expect(element, attrName, content); + } + } + + function getText(element) { + element = element[0] || element; + var walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false); + var text = ''; + + var node; + while (node = walker.nextNode()) { + if (!isAriaHiddenNode(node)) { + text += node.textContent; + } + } + + return text.trim() || ''; + + function isAriaHiddenNode(node) { + while (node.parentNode && (node = node.parentNode) !== element) { + if (node.getAttribute && node.getAttribute('aria-hidden') === 'true') { + return true; + } + } + } + } + + function childHasAttribute(node, attrName) { + var hasChildren = node.hasChildNodes(), + hasAttr = false; + + function isHidden(el) { + var style = el.currentStyle ? el.currentStyle : $window.getComputedStyle(el); + return (style.display === 'none'); + } + + if (hasChildren) { + var children = node.childNodes; + for (var i=0; i < children.length; i++) { + var child = children[i]; + if (child.nodeType === 1 && child.hasAttribute(attrName)) { + if (!isHidden(child)) { + hasAttr = true; + } + } + } + } + + return hasAttr; + } +} + +})(); +(function(){ +"use strict"; + + +mdCompilerService.$inject = ["$q", "$templateRequest", "$injector", "$compile", "$controller"];angular + .module('material.core') + .service('$mdCompiler', mdCompilerService); + +function mdCompilerService($q, $templateRequest, $injector, $compile, $controller) { + /* jshint validthis: true */ + + /* + * @ngdoc service + * @name $mdCompiler + * @module material.core + * @description + * The $mdCompiler service is an abstraction of angular's compiler, that allows the developer + * to easily compile an element with a templateUrl, controller, and locals. + * + * @usage + * + * $mdCompiler.compile({ + * templateUrl: 'modal.html', + * controller: 'ModalCtrl', + * locals: { + * modal: myModalInstance; + * } + * }).then(function(compileData) { + * compileData.element; // modal.html's template in an element + * compileData.link(myScope); //attach controller & scope to element + * }); + * + */ + + /* + * @ngdoc method + * @name $mdCompiler#compile + * @description A helper to compile an HTML template/templateUrl with a given controller, + * locals, and scope. + * @param {object} options An options object, with the following properties: + * + * - `controller` - `{(string=|function()=}` Controller fn that should be associated with + * newly created scope or the name of a registered controller if passed as a string. + * - `controllerAs` - `{string=}` A controller alias name. If present the controller will be + * published to scope under the `controllerAs` name. + * - `template` - `{string=}` An html template as a string. + * - `templateUrl` - `{string=}` A path to an html template. + * - `transformTemplate` - `{function(template)=}` A function which transforms the template after + * it is loaded. It will be given the template string as a parameter, and should + * return a a new string representing the transformed template. + * - `resolve` - `{Object.=}` - An optional map of dependencies which should + * be injected into the controller. If any of these dependencies are promises, the compiler + * will wait for them all to be resolved, or if one is rejected before the controller is + * instantiated `compile()` will fail.. + * * `key` - `{string}`: a name of a dependency to be injected into the controller. + * * `factory` - `{string|function}`: If `string` then it is an alias for a service. + * Otherwise if function, then it is injected and the return value is treated as the + * dependency. If the result is a promise, it is resolved before its value is + * injected into the controller. + * + * @returns {object=} promise A promise, which will be resolved with a `compileData` object. + * `compileData` has the following properties: + * + * - `element` - `{element}`: an uncompiled element matching the provided template. + * - `link` - `{function(scope)}`: A link function, which, when called, will compile + * the element and instantiate the provided controller (if given). + * - `locals` - `{object}`: The locals which will be passed into the controller once `link` is + * called. If `bindToController` is true, they will be coppied to the ctrl instead + * - `bindToController` - `bool`: bind the locals to the controller, instead of passing them in. + */ + this.compile = function(options) { + var templateUrl = options.templateUrl; + var template = options.template || ''; + var controller = options.controller; + var controllerAs = options.controllerAs; + var resolve = angular.extend({}, options.resolve || {}); + var locals = angular.extend({}, options.locals || {}); + var transformTemplate = options.transformTemplate || angular.identity; + var bindToController = options.bindToController; + + // Take resolve values and invoke them. + // Resolves can either be a string (value: 'MyRegisteredAngularConst'), + // or an invokable 'factory' of sorts: (value: function ValueGetter($dependency) {}) + angular.forEach(resolve, function(value, key) { + if (angular.isString(value)) { + resolve[key] = $injector.get(value); + } else { + resolve[key] = $injector.invoke(value); + } + }); + //Add the locals, which are just straight values to inject + //eg locals: { three: 3 }, will inject three into the controller + angular.extend(resolve, locals); + + if (templateUrl) { + resolve.$template = $templateRequest(templateUrl) + .then(function(response) { + return response; + }); + } else { + resolve.$template = $q.when(template); + } + + // Wait for all the resolves to finish if they are promises + return $q.all(resolve).then(function(locals) { + + var compiledData; + var template = transformTemplate(locals.$template, options); + var element = options.element || angular.element('
      ').html(template.trim()).contents(); + var linkFn = $compile(element); + + // Return a linking function that can be used later when the element is ready + return compiledData = { + locals: locals, + element: element, + link: function link(scope) { + locals.$scope = scope; + + //Instantiate controller if it exists, because we have scope + if (controller) { + var invokeCtrl = $controller(controller, locals, true, controllerAs); + if (bindToController) { + angular.extend(invokeCtrl.instance, locals); + } + var ctrl = invokeCtrl(); + //See angular-route source for this logic + element.data('$ngControllerController', ctrl); + element.children().data('$ngControllerController', ctrl); + + // Publish reference to this controller + compiledData.controller = ctrl; + } + return linkFn(scope); + } + }; + }); + + }; +} + +})(); +(function(){ +"use strict"; + + +MdGesture.$inject = ["$$MdGestureHandler", "$$rAF", "$timeout"]; +attachToDocument.$inject = ["$mdGesture", "$$MdGestureHandler"];var HANDLERS = {}; + +/* The state of the current 'pointer' + * The pointer represents the state of the current touch. + * It contains normalized x and y coordinates from DOM events, + * as well as other information abstracted from the DOM. + */ + +var pointer, lastPointer, forceSkipClickHijack = false; + +/** + * The position of the most recent click if that click was on a label element. + * @type {{x: number, y: number}?} + */ +var lastLabelClickPos = null; + +// Used to attach event listeners once when multiple ng-apps are running. +var isInitialized = false; + +angular + .module('material.core.gestures', [ ]) + .provider('$mdGesture', MdGestureProvider) + .factory('$$MdGestureHandler', MdGestureHandler) + .run( attachToDocument ); + +/** + * @ngdoc service + * @name $mdGestureProvider + * @module material.core.gestures + * + * @description + * In some scenarios on Mobile devices (without jQuery), the click events should NOT be hijacked. + * `$mdGestureProvider` is used to configure the Gesture module to ignore or skip click hijacking on mobile + * devices. + * + * + * app.config(function($mdGestureProvider) { + * + * // For mobile devices without jQuery loaded, do not + * // intercept click events during the capture phase. + * $mdGestureProvider.skipClickHijack(); + * + * }); + * + * + */ +function MdGestureProvider() { } + +MdGestureProvider.prototype = { + + // Publish access to setter to configure a variable BEFORE the + // $mdGesture service is instantiated... + skipClickHijack: function() { + return forceSkipClickHijack = true; + }, + + /** + * $get is used to build an instance of $mdGesture + * @ngInject + */ + $get : ["$$MdGestureHandler", "$$rAF", "$timeout", function($$MdGestureHandler, $$rAF, $timeout) { + return new MdGesture($$MdGestureHandler, $$rAF, $timeout); + }] +}; + + + +/** + * MdGesture factory construction function + * @ngInject + */ +function MdGesture($$MdGestureHandler, $$rAF, $timeout) { + var userAgent = navigator.userAgent || navigator.vendor || window.opera; + var isIos = userAgent.match(/ipad|iphone|ipod/i); + var isAndroid = userAgent.match(/android/i); + var touchActionProperty = getTouchAction(); + var hasJQuery = (typeof window.jQuery !== 'undefined') && (angular.element === window.jQuery); + + var self = { + handler: addHandler, + register: register, + isIos: isIos, + isAndroid: isAndroid, + // On mobile w/out jQuery, we normally intercept clicks. Should we skip that? + isHijackingClicks: (isIos || isAndroid) && !hasJQuery && !forceSkipClickHijack + }; + + if (self.isHijackingClicks) { + var maxClickDistance = 6; + self.handler('click', { + options: { + maxDistance: maxClickDistance + }, + onEnd: checkDistanceAndEmit('click') + }); + + self.handler('focus', { + options: { + maxDistance: maxClickDistance + }, + onEnd: function(ev, pointer) { + if (pointer.distance < this.state.options.maxDistance) { + if (canFocus(ev.target)) { + this.dispatchEvent(ev, 'focus', pointer); + ev.target.focus(); + } + } + + function canFocus(element) { + var focusableElements = ['INPUT', 'SELECT', 'BUTTON', 'TEXTAREA', 'VIDEO', 'AUDIO']; + + return (element.getAttribute('tabindex') != '-1') && + !element.hasAttribute('DISABLED') && + (element.hasAttribute('tabindex') || element.hasAttribute('href') || element.isContentEditable || + (focusableElements.indexOf(element.nodeName) != -1)); + } + } + }); + + self.handler('mouseup', { + options: { + maxDistance: maxClickDistance + }, + onEnd: checkDistanceAndEmit('mouseup') + }); + + self.handler('mousedown', { + onStart: function(ev) { + this.dispatchEvent(ev, 'mousedown'); + } + }); + } + + function checkDistanceAndEmit(eventName) { + return function(ev, pointer) { + if (pointer.distance < this.state.options.maxDistance) { + this.dispatchEvent(ev, eventName, pointer); + } + }; + } + + /* + * Register an element to listen for a handler. + * This allows an element to override the default options for a handler. + * Additionally, some handlers like drag and hold only dispatch events if + * the domEvent happens inside an element that's registered to listen for these events. + * + * @see GestureHandler for how overriding of default options works. + * @example $mdGesture.register(myElement, 'drag', { minDistance: 20, horziontal: false }) + */ + function register(element, handlerName, options) { + var handler = HANDLERS[handlerName.replace(/^\$md./, '')]; + if (!handler) { + throw new Error('Failed to register element with handler ' + handlerName + '. ' + + 'Available handlers: ' + Object.keys(HANDLERS).join(', ')); + } + return handler.registerElement(element, options); + } + + /* + * add a handler to $mdGesture. see below. + */ + function addHandler(name, definition) { + var handler = new $$MdGestureHandler(name); + angular.extend(handler, definition); + HANDLERS[name] = handler; + + return self; + } + + /* + * Register handlers. These listen to touch/start/move events, interpret them, + * and dispatch gesture events depending on options & conditions. These are all + * instances of GestureHandler. + * @see GestureHandler + */ + return self + /* + * The press handler dispatches an event on touchdown/touchend. + * It's a simple abstraction of touch/mouse/pointer start and end. + */ + .handler('press', { + onStart: function (ev, pointer) { + this.dispatchEvent(ev, '$md.pressdown'); + }, + onEnd: function (ev, pointer) { + this.dispatchEvent(ev, '$md.pressup'); + } + }) + + /* + * The hold handler dispatches an event if the user keeps their finger within + * the same area for ms. + * The hold handler will only run if a parent of the touch target is registered + * to listen for hold events through $mdGesture.register() + */ + .handler('hold', { + options: { + maxDistance: 6, + delay: 500 + }, + onCancel: function () { + $timeout.cancel(this.state.timeout); + }, + onStart: function (ev, pointer) { + // For hold, require a parent to be registered with $mdGesture.register() + // Because we prevent scroll events, this is necessary. + if (!this.state.registeredParent) return this.cancel(); + + this.state.pos = {x: pointer.x, y: pointer.y}; + this.state.timeout = $timeout(angular.bind(this, function holdDelayFn() { + this.dispatchEvent(ev, '$md.hold'); + this.cancel(); //we're done! + }), this.state.options.delay, false); + }, + onMove: function (ev, pointer) { + // Don't scroll while waiting for hold. + // If we don't preventDefault touchmove events here, Android will assume we don't + // want to listen to anymore touch events. It will start scrolling and stop sending + // touchmove events. + if (!touchActionProperty && ev.type === 'touchmove') ev.preventDefault(); + + // If the user moves greater than pixels, stop the hold timer + // set in onStart + var dx = this.state.pos.x - pointer.x; + var dy = this.state.pos.y - pointer.y; + if (Math.sqrt(dx * dx + dy * dy) > this.options.maxDistance) { + this.cancel(); + } + }, + onEnd: function () { + this.onCancel(); + } + }) + + /* + * The drag handler dispatches a drag event if the user holds and moves his finger greater than + * px in the x or y direction, depending on options.horizontal. + * The drag will be cancelled if the user moves his finger greater than * in + * the perpendicular direction. Eg if the drag is horizontal and the user moves his finger * + * pixels vertically, this handler won't consider the move part of a drag. + */ + .handler('drag', { + options: { + minDistance: 6, + horizontal: true, + cancelMultiplier: 1.5 + }, + onSetup: function(element, options) { + if (touchActionProperty) { + // We check for horizontal to be false, because otherwise we would overwrite the default opts. + this.oldTouchAction = element[0].style[touchActionProperty]; + element[0].style[touchActionProperty] = options.horizontal === false ? 'pan-y' : 'pan-x'; + } + }, + onCleanup: function(element) { + if (this.oldTouchAction) { + element[0].style[touchActionProperty] = this.oldTouchAction; + } + }, + onStart: function (ev) { + // For drag, require a parent to be registered with $mdGesture.register() + if (!this.state.registeredParent) this.cancel(); + }, + onMove: function (ev, pointer) { + var shouldStartDrag, shouldCancel; + // Don't scroll while deciding if this touchmove qualifies as a drag event. + // If we don't preventDefault touchmove events here, Android will assume we don't + // want to listen to anymore touch events. It will start scrolling and stop sending + // touchmove events. + if (!touchActionProperty && ev.type === 'touchmove') ev.preventDefault(); + + if (!this.state.dragPointer) { + if (this.state.options.horizontal) { + shouldStartDrag = Math.abs(pointer.distanceX) > this.state.options.minDistance; + shouldCancel = Math.abs(pointer.distanceY) > this.state.options.minDistance * this.state.options.cancelMultiplier; + } else { + shouldStartDrag = Math.abs(pointer.distanceY) > this.state.options.minDistance; + shouldCancel = Math.abs(pointer.distanceX) > this.state.options.minDistance * this.state.options.cancelMultiplier; + } + + if (shouldStartDrag) { + // Create a new pointer representing this drag, starting at this point where the drag started. + this.state.dragPointer = makeStartPointer(ev); + updatePointerState(ev, this.state.dragPointer); + this.dispatchEvent(ev, '$md.dragstart', this.state.dragPointer); + + } else if (shouldCancel) { + this.cancel(); + } + } else { + this.dispatchDragMove(ev); + } + }, + // Only dispatch dragmove events every frame; any more is unnecessary + dispatchDragMove: $$rAF.throttle(function (ev) { + // Make sure the drag didn't stop while waiting for the next frame + if (this.state.isRunning) { + updatePointerState(ev, this.state.dragPointer); + this.dispatchEvent(ev, '$md.drag', this.state.dragPointer); + } + }), + onEnd: function (ev, pointer) { + if (this.state.dragPointer) { + updatePointerState(ev, this.state.dragPointer); + this.dispatchEvent(ev, '$md.dragend', this.state.dragPointer); + } + } + }) + + /* + * The swipe handler will dispatch a swipe event if, on the end of a touch, + * the velocity and distance were high enough. + */ + .handler('swipe', { + options: { + minVelocity: 0.65, + minDistance: 10 + }, + onEnd: function (ev, pointer) { + var eventType; + + if (Math.abs(pointer.velocityX) > this.state.options.minVelocity && + Math.abs(pointer.distanceX) > this.state.options.minDistance) { + eventType = pointer.directionX == 'left' ? '$md.swipeleft' : '$md.swiperight'; + this.dispatchEvent(ev, eventType); + } + else if (Math.abs(pointer.velocityY) > this.state.options.minVelocity && + Math.abs(pointer.distanceY) > this.state.options.minDistance) { + eventType = pointer.directionY == 'up' ? '$md.swipeup' : '$md.swipedown'; + this.dispatchEvent(ev, eventType); + } + } + }); + + function getTouchAction() { + var testEl = document.createElement('div'); + var vendorPrefixes = ['', 'webkit', 'Moz', 'MS', 'ms', 'o']; + + for (var i = 0; i < vendorPrefixes.length; i++) { + var prefix = vendorPrefixes[i]; + var property = prefix ? prefix + 'TouchAction' : 'touchAction'; + if (angular.isDefined(testEl.style[property])) { + return property; + } + } + } + +} + +/** + * MdGestureHandler + * A GestureHandler is an object which is able to dispatch custom dom events + * based on native dom {touch,pointer,mouse}{start,move,end} events. + * + * A gesture will manage its lifecycle through the start,move,end, and cancel + * functions, which are called by native dom events. + * + * A gesture has the concept of 'options' (eg a swipe's required velocity), which can be + * overridden by elements registering through $mdGesture.register() + */ +function GestureHandler (name) { + this.name = name; + this.state = {}; +} + +function MdGestureHandler() { + var hasJQuery = (typeof window.jQuery !== 'undefined') && (angular.element === window.jQuery); + + GestureHandler.prototype = { + options: {}, + // jQuery listeners don't work with custom DOMEvents, so we have to dispatch events + // differently when jQuery is loaded + dispatchEvent: hasJQuery ? jQueryDispatchEvent : nativeDispatchEvent, + + // These are overridden by the registered handler + onSetup: angular.noop, + onCleanup: angular.noop, + onStart: angular.noop, + onMove: angular.noop, + onEnd: angular.noop, + onCancel: angular.noop, + + // onStart sets up a new state for the handler, which includes options from the + // nearest registered parent element of ev.target. + start: function (ev, pointer) { + if (this.state.isRunning) return; + var parentTarget = this.getNearestParent(ev.target); + // Get the options from the nearest registered parent + var parentTargetOptions = parentTarget && parentTarget.$mdGesture[this.name] || {}; + + this.state = { + isRunning: true, + // Override the default options with the nearest registered parent's options + options: angular.extend({}, this.options, parentTargetOptions), + // Pass in the registered parent node to the state so the onStart listener can use + registeredParent: parentTarget + }; + this.onStart(ev, pointer); + }, + move: function (ev, pointer) { + if (!this.state.isRunning) return; + this.onMove(ev, pointer); + }, + end: function (ev, pointer) { + if (!this.state.isRunning) return; + this.onEnd(ev, pointer); + this.state.isRunning = false; + }, + cancel: function (ev, pointer) { + this.onCancel(ev, pointer); + this.state = {}; + }, + + // Find and return the nearest parent element that has been registered to + // listen for this handler via $mdGesture.register(element, 'handlerName'). + getNearestParent: function (node) { + var current = node; + while (current) { + if ((current.$mdGesture || {})[this.name]) { + return current; + } + current = current.parentNode; + } + return null; + }, + + // Called from $mdGesture.register when an element registers itself with a handler. + // Store the options the user gave on the DOMElement itself. These options will + // be retrieved with getNearestParent when the handler starts. + registerElement: function (element, options) { + var self = this; + element[0].$mdGesture = element[0].$mdGesture || {}; + element[0].$mdGesture[this.name] = options || {}; + element.on('$destroy', onDestroy); + + self.onSetup(element, options || {}); + + return onDestroy; + + function onDestroy() { + delete element[0].$mdGesture[self.name]; + element.off('$destroy', onDestroy); + + self.onCleanup(element, options || {}); + } + } + }; + + return GestureHandler; + + /* + * Dispatch an event with jQuery + * TODO: Make sure this sends bubbling events + * + * @param srcEvent the original DOM touch event that started this. + * @param eventType the name of the custom event to send (eg 'click' or '$md.drag') + * @param eventPointer the pointer object that matches this event. + */ + function jQueryDispatchEvent(srcEvent, eventType, eventPointer) { + eventPointer = eventPointer || pointer; + var eventObj = new angular.element.Event(eventType); + + eventObj.$material = true; + eventObj.pointer = eventPointer; + eventObj.srcEvent = srcEvent; + + angular.extend(eventObj, { + clientX: eventPointer.x, + clientY: eventPointer.y, + screenX: eventPointer.x, + screenY: eventPointer.y, + pageX: eventPointer.x, + pageY: eventPointer.y, + ctrlKey: srcEvent.ctrlKey, + altKey: srcEvent.altKey, + shiftKey: srcEvent.shiftKey, + metaKey: srcEvent.metaKey + }); + angular.element(eventPointer.target).trigger(eventObj); + } + + /* + * NOTE: nativeDispatchEvent is very performance sensitive. + * @param srcEvent the original DOM touch event that started this. + * @param eventType the name of the custom event to send (eg 'click' or '$md.drag') + * @param eventPointer the pointer object that matches this event. + */ + function nativeDispatchEvent(srcEvent, eventType, eventPointer) { + eventPointer = eventPointer || pointer; + var eventObj; + + if (eventType === 'click' || eventType == 'mouseup' || eventType == 'mousedown' ) { + eventObj = document.createEvent('MouseEvents'); + eventObj.initMouseEvent( + eventType, true, true, window, srcEvent.detail, + eventPointer.x, eventPointer.y, eventPointer.x, eventPointer.y, + srcEvent.ctrlKey, srcEvent.altKey, srcEvent.shiftKey, srcEvent.metaKey, + srcEvent.button, srcEvent.relatedTarget || null + ); + + } else { + eventObj = document.createEvent('CustomEvent'); + eventObj.initCustomEvent(eventType, true, true, {}); + } + eventObj.$material = true; + eventObj.pointer = eventPointer; + eventObj.srcEvent = srcEvent; + eventPointer.target.dispatchEvent(eventObj); + } + +} + +/** + * Attach Gestures: hook document and check shouldHijack clicks + * @ngInject + */ +function attachToDocument( $mdGesture, $$MdGestureHandler ) { + + // Polyfill document.contains for IE11. + // TODO: move to util + document.contains || (document.contains = function (node) { + return document.body.contains(node); + }); + + if (!isInitialized && $mdGesture.isHijackingClicks ) { + /* + * If hijack clicks is true, we preventDefault any click that wasn't + * sent by ngMaterial. This is because on older Android & iOS, a false, or 'ghost', + * click event will be sent ~400ms after a touchend event happens. + * The only way to know if this click is real is to prevent any normal + * click events, and add a flag to events sent by material so we know not to prevent those. + * + * Two exceptions to click events that should be prevented are: + * - click events sent by the keyboard (eg form submit) + * - events that originate from an Ionic app + */ + document.addEventListener('click' , clickHijacker , true); + document.addEventListener('mouseup' , mouseInputHijacker, true); + document.addEventListener('mousedown', mouseInputHijacker, true); + document.addEventListener('focus' , mouseInputHijacker, true); + + isInitialized = true; + } + + function mouseInputHijacker(ev) { + var isKeyClick = !ev.clientX && !ev.clientY; + if (!isKeyClick && !ev.$material && !ev.isIonicTap + && !isInputEventFromLabelClick(ev)) { + ev.preventDefault(); + ev.stopPropagation(); + } + } + + function clickHijacker(ev) { + var isKeyClick = ev.clientX === 0 && ev.clientY === 0; + if (!isKeyClick && !ev.$material && !ev.isIonicTap + && !isInputEventFromLabelClick(ev)) { + ev.preventDefault(); + ev.stopPropagation(); + lastLabelClickPos = null; + } else { + lastLabelClickPos = null; + if (ev.target.tagName.toLowerCase() == 'label') { + lastLabelClickPos = {x: ev.x, y: ev.y}; + } + } + } + + + // Listen to all events to cover all platforms. + var START_EVENTS = 'mousedown touchstart pointerdown'; + var MOVE_EVENTS = 'mousemove touchmove pointermove'; + var END_EVENTS = 'mouseup mouseleave touchend touchcancel pointerup pointercancel'; + + angular.element(document) + .on(START_EVENTS, gestureStart) + .on(MOVE_EVENTS, gestureMove) + .on(END_EVENTS, gestureEnd) + // For testing + .on('$$mdGestureReset', function gestureClearCache () { + lastPointer = pointer = null; + }); + + /* + * When a DOM event happens, run all registered gesture handlers' lifecycle + * methods which match the DOM event. + * Eg when a 'touchstart' event happens, runHandlers('start') will call and + * run `handler.cancel()` and `handler.start()` on all registered handlers. + */ + function runHandlers(handlerEvent, event) { + var handler; + for (var name in HANDLERS) { + handler = HANDLERS[name]; + if( handler instanceof $$MdGestureHandler ) { + + if (handlerEvent === 'start') { + // Run cancel to reset any handlers' state + handler.cancel(); + } + handler[handlerEvent](event, pointer); + + } + } + } + + /* + * gestureStart vets if a start event is legitimate (and not part of a 'ghost click' from iOS/Android) + * If it is legitimate, we initiate the pointer state and mark the current pointer's type + * For example, for a touchstart event, mark the current pointer as a 'touch' pointer, so mouse events + * won't effect it. + */ + function gestureStart(ev) { + // If we're already touched down, abort + if (pointer) return; + + var now = +Date.now(); + + // iOS & old android bug: after a touch event, a click event is sent 350 ms later. + // If <400ms have passed, don't allow an event of a different type than the previous event + if (lastPointer && !typesMatch(ev, lastPointer) && (now - lastPointer.endTime < 1500)) { + return; + } + + pointer = makeStartPointer(ev); + + runHandlers('start', ev); + } + /* + * If a move event happens of the right type, update the pointer and run all the move handlers. + * "of the right type": if a mousemove happens but our pointer started with a touch event, do nothing. + */ + function gestureMove(ev) { + if (!pointer || !typesMatch(ev, pointer)) return; + + updatePointerState(ev, pointer); + runHandlers('move', ev); + } + /* + * If an end event happens of the right type, update the pointer, run endHandlers, and save the pointer as 'lastPointer' + */ + function gestureEnd(ev) { + if (!pointer || !typesMatch(ev, pointer)) return; + + updatePointerState(ev, pointer); + pointer.endTime = +Date.now(); + + runHandlers('end', ev); + + lastPointer = pointer; + pointer = null; + } + +} + +// ******************** +// Module Functions +// ******************** + +/* + * Initiate the pointer. x, y, and the pointer's type. + */ +function makeStartPointer(ev) { + var point = getEventPoint(ev); + var startPointer = { + startTime: +Date.now(), + target: ev.target, + // 'p' for pointer events, 'm' for mouse, 't' for touch + type: ev.type.charAt(0) + }; + startPointer.startX = startPointer.x = point.pageX; + startPointer.startY = startPointer.y = point.pageY; + return startPointer; +} + +/* + * return whether the pointer's type matches the event's type. + * Eg if a touch event happens but the pointer has a mouse type, return false. + */ +function typesMatch(ev, pointer) { + return ev && pointer && ev.type.charAt(0) === pointer.type; +} + +/** + * Gets whether the given event is an input event that was caused by clicking on an + * associated label element. + * + * This is necessary because the browser will, upon clicking on a label element, fire an + * *extra* click event on its associated input (if any). mdGesture is able to flag the label + * click as with `$material` correctly, but not the second input click. + * + * In order to determine whether an input event is from a label click, we compare the (x, y) for + * the event to the (x, y) for the most recent label click (which is cleared whenever a non-label + * click occurs). Unfortunately, there are no event properties that tie the input and the label + * together (such as relatedTarget). + * + * @param {MouseEvent} event + * @returns {boolean} + */ +function isInputEventFromLabelClick(event) { + return lastLabelClickPos + && lastLabelClickPos.x == event.x + && lastLabelClickPos.y == event.y; +} + +/* + * Update the given pointer based upon the given DOMEvent. + * Distance, velocity, direction, duration, etc + */ +function updatePointerState(ev, pointer) { + var point = getEventPoint(ev); + var x = pointer.x = point.pageX; + var y = pointer.y = point.pageY; + + pointer.distanceX = x - pointer.startX; + pointer.distanceY = y - pointer.startY; + pointer.distance = Math.sqrt( + pointer.distanceX * pointer.distanceX + pointer.distanceY * pointer.distanceY + ); + + pointer.directionX = pointer.distanceX > 0 ? 'right' : pointer.distanceX < 0 ? 'left' : ''; + pointer.directionY = pointer.distanceY > 0 ? 'down' : pointer.distanceY < 0 ? 'up' : ''; + + pointer.duration = +Date.now() - pointer.startTime; + pointer.velocityX = pointer.distanceX / pointer.duration; + pointer.velocityY = pointer.distanceY / pointer.duration; +} + +/* + * Normalize the point where the DOM event happened whether it's touch or mouse. + * @returns point event obj with pageX and pageY on it. + */ +function getEventPoint(ev) { + ev = ev.originalEvent || ev; // support jQuery events + return (ev.touches && ev.touches[0]) || + (ev.changedTouches && ev.changedTouches[0]) || + ev; +} + +})(); +(function(){ +"use strict"; + +angular.module('material.core') + .provider('$$interimElement', InterimElementProvider); + +/* + * @ngdoc service + * @name $$interimElement + * @module material.core + * + * @description + * + * Factory that contructs `$$interimElement.$service` services. + * Used internally in material design for elements that appear on screen temporarily. + * The service provides a promise-like API for interacting with the temporary + * elements. + * + * ```js + * app.service('$mdToast', function($$interimElement) { + * var $mdToast = $$interimElement(toastDefaultOptions); + * return $mdToast; + * }); + * ``` + * @param {object=} defaultOptions Options used by default for the `show` method on the service. + * + * @returns {$$interimElement.$service} + * + */ + +function InterimElementProvider() { + InterimElementFactory.$inject = ["$document", "$q", "$$q", "$rootScope", "$timeout", "$rootElement", "$animate", "$mdUtil", "$mdCompiler", "$mdTheming", "$injector"]; + createInterimElementProvider.$get = InterimElementFactory; + return createInterimElementProvider; + + /** + * Returns a new provider which allows configuration of a new interimElement + * service. Allows configuration of default options & methods for options, + * as well as configuration of 'preset' methods (eg dialog.basic(): basic is a preset method) + */ + function createInterimElementProvider(interimFactoryName) { + factory.$inject = ["$$interimElement", "$injector"]; + var EXPOSED_METHODS = ['onHide', 'onShow', 'onRemove']; + + var customMethods = {}; + var providerConfig = { + presets: {} + }; + + var provider = { + setDefaults: setDefaults, + addPreset: addPreset, + addMethod: addMethod, + $get: factory + }; + + /** + * all interim elements will come with the 'build' preset + */ + provider.addPreset('build', { + methods: ['controller', 'controllerAs', 'resolve', + 'template', 'templateUrl', 'themable', 'transformTemplate', 'parent'] + }); + + return provider; + + /** + * Save the configured defaults to be used when the factory is instantiated + */ + function setDefaults(definition) { + providerConfig.optionsFactory = definition.options; + providerConfig.methods = (definition.methods || []).concat(EXPOSED_METHODS); + return provider; + } + + /** + * Add a method to the factory that isn't specific to any interim element operations + */ + + function addMethod(name, fn) { + customMethods[name] = fn; + return provider; + } + + /** + * Save the configured preset to be used when the factory is instantiated + */ + function addPreset(name, definition) { + definition = definition || {}; + definition.methods = definition.methods || []; + definition.options = definition.options || function() { return {}; }; + + if (/^cancel|hide|show$/.test(name)) { + throw new Error("Preset '" + name + "' in " + interimFactoryName + " is reserved!"); + } + if (definition.methods.indexOf('_options') > -1) { + throw new Error("Method '_options' in " + interimFactoryName + " is reserved!"); + } + providerConfig.presets[name] = { + methods: definition.methods.concat(EXPOSED_METHODS), + optionsFactory: definition.options, + argOption: definition.argOption + }; + return provider; + } + + function addPresetMethod(presetName, methodName, method) { + providerConfig.presets[presetName][methodName] = method; + } + + /** + * Create a factory that has the given methods & defaults implementing interimElement + */ + /* @ngInject */ + function factory($$interimElement, $injector) { + var defaultMethods; + var defaultOptions; + var interimElementService = $$interimElement(); + + /* + * publicService is what the developer will be using. + * It has methods hide(), cancel(), show(), build(), and any other + * presets which were set during the config phase. + */ + var publicService = { + hide: interimElementService.hide, + cancel: interimElementService.cancel, + show: showInterimElement, + + // Special internal method to destroy an interim element without animations + // used when navigation changes causes a $scope.$destroy() action + destroy : destroyInterimElement + }; + + + defaultMethods = providerConfig.methods || []; + // This must be invoked after the publicService is initialized + defaultOptions = invokeFactory(providerConfig.optionsFactory, {}); + + // Copy over the simple custom methods + angular.forEach(customMethods, function(fn, name) { + publicService[name] = fn; + }); + + angular.forEach(providerConfig.presets, function(definition, name) { + var presetDefaults = invokeFactory(definition.optionsFactory, {}); + var presetMethods = (definition.methods || []).concat(defaultMethods); + + // Every interimElement built with a preset has a field called `$type`, + // which matches the name of the preset. + // Eg in preset 'confirm', options.$type === 'confirm' + angular.extend(presetDefaults, { $type: name }); + + // This creates a preset class which has setter methods for every + // method given in the `.addPreset()` function, as well as every + // method given in the `.setDefaults()` function. + // + // @example + // .setDefaults({ + // methods: ['hasBackdrop', 'clickOutsideToClose', 'escapeToClose', 'targetEvent'], + // options: dialogDefaultOptions + // }) + // .addPreset('alert', { + // methods: ['title', 'ok'], + // options: alertDialogOptions + // }) + // + // Set values will be passed to the options when interimElement.show() is called. + function Preset(opts) { + this._options = angular.extend({}, presetDefaults, opts); + } + angular.forEach(presetMethods, function(name) { + Preset.prototype[name] = function(value) { + this._options[name] = value; + return this; + }; + }); + + // Create shortcut method for one-linear methods + if (definition.argOption) { + var methodName = 'show' + name.charAt(0).toUpperCase() + name.slice(1); + publicService[methodName] = function(arg) { + var config = publicService[name](arg); + return publicService.show(config); + }; + } + + // eg $mdDialog.alert() will return a new alert preset + publicService[name] = function(arg) { + // If argOption is supplied, eg `argOption: 'content'`, then we assume + // if the argument is not an options object then it is the `argOption` option. + // + // @example `$mdToast.simple('hello')` // sets options.content to hello + // // because argOption === 'content' + if (arguments.length && definition.argOption && + !angular.isObject(arg) && !angular.isArray(arg)) { + + return (new Preset())[definition.argOption](arg); + + } else { + return new Preset(arg); + } + + }; + }); + + return publicService; + + /** + * + */ + function showInterimElement(opts) { + // opts is either a preset which stores its options on an _options field, + // or just an object made up of options + opts = opts || { }; + if (opts._options) opts = opts._options; + + return interimElementService.show( + angular.extend({}, defaultOptions, opts) + ); + } + + /** + * Special method to hide and destroy an interimElement WITHOUT + * any 'leave` or hide animations ( an immediate force hide/remove ) + * + * NOTE: This calls the onRemove() subclass method for each component... + * which must have code to respond to `options.$destroy == true` + */ + function destroyInterimElement(opts) { + return interimElementService.destroy(opts); + } + + /** + * Helper to call $injector.invoke with a local of the factory name for + * this provider. + * If an $mdDialog is providing options for a dialog and tries to inject + * $mdDialog, a circular dependency error will happen. + * We get around that by manually injecting $mdDialog as a local. + */ + function invokeFactory(factory, defaultVal) { + var locals = {}; + locals[interimFactoryName] = publicService; + return $injector.invoke(factory || function() { return defaultVal; }, {}, locals); + } + + } + + } + + /* @ngInject */ + function InterimElementFactory($document, $q, $$q, $rootScope, $timeout, $rootElement, $animate, + $mdUtil, $mdCompiler, $mdTheming, $injector ) { + return function createInterimElementService() { + var SHOW_CANCELLED = false; + + /* + * @ngdoc service + * @name $$interimElement.$service + * + * @description + * A service used to control inserting and removing an element into the DOM. + * + */ + var service, stack = []; + + // Publish instance $$interimElement service; + // ... used as $mdDialog, $mdToast, $mdMenu, and $mdSelect + + return service = { + show: show, + hide: hide, + cancel: cancel, + destroy : destroy, + $injector_: $injector + }; + + /* + * @ngdoc method + * @name $$interimElement.$service#show + * @kind function + * + * @description + * Adds the `$interimElement` to the DOM and returns a special promise that will be resolved or rejected + * with hide or cancel, respectively. To external cancel/hide, developers should use the + * + * @param {*} options is hashMap of settings + * @returns a Promise + * + */ + function show(options) { + options = options || {}; + var interimElement = new InterimElement(options || {}); + // When an interim element is currently showing, we have to cancel it. + // Just hiding it, will resolve the InterimElement's promise, the promise should be + // rejected instead. + var hideExisting = !options.skipHide && stack.length ? service.cancel() : $q.when(true); + + // This hide()s only the current interim element before showing the next, new one + // NOTE: this is not reversible (e.g. interim elements are not stackable) + + hideExisting.finally(function() { + + stack.push(interimElement); + interimElement + .show() + .catch(function( reason ) { + //$log.error("InterimElement.show() error: " + reason ); + return reason; + }); + + }); + + // Return a promise that will be resolved when the interim + // element is hidden or cancelled... + + return interimElement.deferred.promise; + } + + /* + * @ngdoc method + * @name $$interimElement.$service#hide + * @kind function + * + * @description + * Removes the `$interimElement` from the DOM and resolves the promise returned from `show` + * + * @param {*} resolveParam Data to resolve the promise with + * @returns a Promise that will be resolved after the element has been removed. + * + */ + function hide(reason, options) { + if ( !stack.length ) return $q.when(reason); + options = options || {}; + + if (options.closeAll) { + var promise = $q.all(stack.reverse().map(closeElement)); + stack = []; + return promise; + } else if (options.closeTo !== undefined) { + return $q.all(stack.splice(options.closeTo).map(closeElement)); + } else { + var interim = stack.pop(); + return closeElement(interim); + } + + function closeElement(interim) { + interim + .remove(reason, false, options || { }) + .catch(function( reason ) { + //$log.error("InterimElement.hide() error: " + reason ); + return reason; + }); + return interim.deferred.promise; + } + } + + /* + * @ngdoc method + * @name $$interimElement.$service#cancel + * @kind function + * + * @description + * Removes the `$interimElement` from the DOM and rejects the promise returned from `show` + * + * @param {*} reason Data to reject the promise with + * @returns Promise that will be resolved after the element has been removed. + * + */ + function cancel(reason, options) { + var interim = stack.pop(); + if ( !interim ) return $q.when(reason); + + interim + .remove(reason, true, options || { }) + .catch(function( reason ) { + //$log.error("InterimElement.cancel() error: " + reason ); + return reason; + }); + + // Since Angular 1.6.7, promises will be logged to $exceptionHandler when the promise + // is not handling the rejection. We create a pseudo catch handler, which will prevent the + // promise from being logged to the $exceptionHandler. + return interim.deferred.promise.catch(angular.noop); + } + + /* + * Special method to quick-remove the interim element without animations + * Note: interim elements are in "interim containers" + */ + function destroy(target) { + var interim = !target ? stack.shift() : null; + var cntr = angular.element(target).length ? angular.element(target)[0].parentNode : null; + + if (cntr) { + // Try to find the interim element in the stack which corresponds to the supplied DOM element. + var filtered = stack.filter(function(entry) { + var currNode = entry.options.element[0]; + return (currNode === cntr); + }); + + // Note: this function might be called when the element already has been removed, in which + // case we won't find any matches. That's ok. + if (filtered.length > 0) { + interim = filtered[0]; + stack.splice(stack.indexOf(interim), 1); + } + } + + return interim ? interim.remove(SHOW_CANCELLED, false, {'$destroy':true}) : $q.when(SHOW_CANCELLED); + } + + /* + * Internal Interim Element Object + * Used internally to manage the DOM element and related data + */ + function InterimElement(options) { + var self, element, showAction = $q.when(true); + + options = configureScopeAndTransitions(options); + + return self = { + options : options, + deferred: $q.defer(), + show : createAndTransitionIn, + remove : transitionOutAndRemove + }; + + /** + * Compile, link, and show this interim element + * Use optional autoHided and transition-in effects + */ + function createAndTransitionIn() { + return $q(function(resolve, reject) { + + // Trigger onCompiling callback before the compilation starts. + // This is useful, when modifying options, which can be influenced by developers. + options.onCompiling && options.onCompiling(options); + + compileElement(options) + .then(function( compiledData ) { + element = linkElement( compiledData, options ); + + showAction = showElement(element, options, compiledData.controller) + .then(resolve, rejectAll); + + }, rejectAll); + + function rejectAll(fault) { + // Force the '$md.show()' promise to reject + self.deferred.reject(fault); + + // Continue rejection propagation + reject(fault); + } + }); + } + + /** + * After the show process has finished/rejected: + * - announce 'removing', + * - perform the transition-out, and + * - perform optional clean up scope. + */ + function transitionOutAndRemove(response, isCancelled, opts) { + + // abort if the show() and compile failed + if ( !element ) return $q.when(false); + + options = angular.extend(options || {}, opts || {}); + options.cancelAutoHide && options.cancelAutoHide(); + options.element.triggerHandler('$mdInterimElementRemove'); + + if ( options.$destroy === true ) { + + return hideElement(options.element, options).then(function(){ + (isCancelled && rejectAll(response)) || resolveAll(response); + }); + + } else { + + $q.when(showAction) + .finally(function() { + hideElement(options.element, options).then(function() { + + (isCancelled && rejectAll(response)) || resolveAll(response); + + }, rejectAll); + }); + + return self.deferred.promise; + } + + + /** + * The `show()` returns a promise that will be resolved when the interim + * element is hidden or cancelled... + */ + function resolveAll(response) { + self.deferred.resolve(response); + } + + /** + * Force the '$md.show()' promise to reject + */ + function rejectAll(fault) { + self.deferred.reject(fault); + } + } + + /** + * Prepare optional isolated scope and prepare $animate with default enter and leave + * transitions for the new element instance. + */ + function configureScopeAndTransitions(options) { + options = options || { }; + if ( options.template ) { + options.template = $mdUtil.processTemplate(options.template); + } + + return angular.extend({ + preserveScope: false, + cancelAutoHide : angular.noop, + scope: options.scope || $rootScope.$new(options.isolateScope), + + /** + * Default usage to enable $animate to transition-in; can be easily overridden via 'options' + */ + onShow: function transitionIn(scope, element, options) { + return $animate.enter(element, options.parent); + }, + + /** + * Default usage to enable $animate to transition-out; can be easily overridden via 'options' + */ + onRemove: function transitionOut(scope, element) { + // Element could be undefined if a new element is shown before + // the old one finishes compiling. + return element && $animate.leave(element) || $q.when(); + } + }, options ); + + } + + /** + * Compile an element with a templateUrl, controller, and locals + */ + function compileElement(options) { + + var compiled = !options.skipCompile ? $mdCompiler.compile(options) : null; + + return compiled || $q(function (resolve) { + resolve({ + locals: {}, + link: function () { + return options.element; + } + }); + }); + } + + /** + * Link an element with compiled configuration + */ + function linkElement(compileData, options){ + angular.extend(compileData.locals, options); + + var element = compileData.link(options.scope); + + // Search for parent at insertion time, if not specified + options.element = element; + options.parent = findParent(element, options); + if (options.themable) $mdTheming(element); + + return element; + } + + /** + * Search for parent at insertion time, if not specified + */ + function findParent(element, options) { + var parent = options.parent; + + // Search for parent at insertion time, if not specified + if (angular.isFunction(parent)) { + parent = parent(options.scope, element, options); + } else if (angular.isString(parent)) { + parent = angular.element($document[0].querySelector(parent)); + } else { + parent = angular.element(parent); + } + + // If parent querySelector/getter function fails, or it's just null, + // find a default. + if (!(parent || {}).length) { + var el; + if ($rootElement[0] && $rootElement[0].querySelector) { + el = $rootElement[0].querySelector(':not(svg) > body'); + } + if (!el) el = $rootElement[0]; + if (el.nodeName == '#comment') { + el = $document[0].body; + } + return angular.element(el); + } + + return parent; + } + + /** + * If auto-hide is enabled, start timer and prepare cancel function + */ + function startAutoHide() { + var autoHideTimer, cancelAutoHide = angular.noop; + + if (options.hideDelay) { + autoHideTimer = $timeout(service.hide, options.hideDelay) ; + cancelAutoHide = function() { + $timeout.cancel(autoHideTimer); + } + } + + // Cache for subsequent use + options.cancelAutoHide = function() { + cancelAutoHide(); + options.cancelAutoHide = undefined; + } + } + + /** + * Show the element ( with transitions), notify complete and start + * optional auto-Hide + */ + function showElement(element, options, controller) { + // Trigger onShowing callback before the `show()` starts + var notifyShowing = options.onShowing || angular.noop; + // Trigger onComplete callback when the `show()` finishes + var notifyComplete = options.onComplete || angular.noop; + + notifyShowing(options.scope, element, options, controller); + + return $q(function (resolve, reject) { + try { + // Start transitionIn + $q.when(options.onShow(options.scope, element, options, controller)) + .then(function () { + notifyComplete(options.scope, element, options); + startAutoHide(); + + resolve(element); + + }, reject ); + + } catch(e) { + reject(e.message); + } + }); + } + + function hideElement(element, options) { + var announceRemoving = options.onRemoving || angular.noop; + + return $$q(function (resolve, reject) { + try { + // Start transitionIn + var action = $$q.when( options.onRemove(options.scope, element, options) || true ); + + // Trigger callback *before* the remove operation starts + announceRemoving(element, action); + + if ( options.$destroy == true ) { + + // For $destroy, onRemove should be synchronous + resolve(element); + + } else { + + // Wait until transition-out is done + action.then(function () { + + if (!options.preserveScope && options.scope ) { + options.scope.$destroy(); + } + + resolve(element); + + }, reject ); + } + + } catch(e) { + reject(e); + } + }); + } + + } + }; + + } + +} + +})(); +(function(){ +"use strict"; + +(function() { + 'use strict'; + + var $mdUtil, $interpolate, $log; + + var SUFFIXES = /(-gt)?-(sm|md|lg|print)/g; + var WHITESPACE = /\s+/g; + + var FLEX_OPTIONS = ['grow', 'initial', 'auto', 'none', 'noshrink', 'nogrow' ]; + var LAYOUT_OPTIONS = ['row', 'column']; + var ALIGNMENT_MAIN_AXIS= [ "", "start", "center", "end", "stretch", "space-around", "space-between" ]; + var ALIGNMENT_CROSS_AXIS= [ "", "start", "center", "end", "stretch" ]; + + var config = { + /** + * Enable directive attribute-to-class conversions + * Developers can use `` to quickly + * disable the Layout directives and prohibit the injection of Layout classNames + */ + enabled: true, + + /** + * List of mediaQuery breakpoints and associated suffixes + * + * [ + * { suffix: "sm", mediaQuery: "screen and (max-width: 599px)" }, + * { suffix: "md", mediaQuery: "screen and (min-width: 600px) and (max-width: 959px)" } + * ] + */ + breakpoints: [] + }; + + registerLayoutAPI( angular.module('material.core.layout', ['ng']) ); + + /** + * registerLayoutAPI() + * + * The original ngMaterial Layout solution used attribute selectors and CSS. + * + * ```html + *
      My Content
      + * ``` + * + * ```css + * [layout] { + * box-sizing: border-box; + * display:flex; + * } + * [layout=column] { + * flex-direction : column + * } + * ``` + * + * Use of attribute selectors creates significant performance impacts in some + * browsers... mainly IE. + * + * This module registers directives that allow the same layout attributes to be + * interpreted and converted to class selectors. The directive will add equivalent classes to each element that + * contains a Layout directive. + * + * ```html + *
      My Content
      + *``` + * + * ```css + * .layout { + * box-sizing: border-box; + * display:flex; + * } + * .layout-column { + * flex-direction : column + * } + * ``` + */ + function registerLayoutAPI(module){ + var PREFIX_REGEXP = /^((?:x|data)[\:\-_])/i; + var SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g; + + // NOTE: these are also defined in constants::MEDIA_PRIORITY and constants::MEDIA + var BREAKPOINTS = [ "", "xs", "gt-xs", "sm", "gt-sm", "md", "gt-md", "lg", "gt-lg", "xl", "print" ]; + var API_WITH_VALUES = [ "layout", "flex", "flex-order", "flex-offset", "layout-align" ]; + var API_NO_VALUES = [ "show", "hide", "layout-padding", "layout-margin" ]; + + + // Build directive registration functions for the standard Layout API... for all breakpoints. + angular.forEach(BREAKPOINTS, function(mqb) { + + // Attribute directives with expected, observable value(s) + angular.forEach( API_WITH_VALUES, function(name){ + var fullName = mqb ? name + "-" + mqb : name; + module.directive( directiveNormalize(fullName), attributeWithObserve(fullName)); + }); + + // Attribute directives with no expected value(s) + angular.forEach( API_NO_VALUES, function(name){ + var fullName = mqb ? name + "-" + mqb : name; + module.directive( directiveNormalize(fullName), attributeWithoutValue(fullName)); + }); + + }); + + // Register other, special directive functions for the Layout features: + module + + .provider('$$mdLayout' , function() { + // Publish internal service for Layouts + return { + $get : angular.noop, + validateAttributeValue : validateAttributeValue, + validateAttributeUsage : validateAttributeUsage, + /** + * Easy way to disable/enable the Layout API. + * When disabled, this stops all attribute-to-classname generations + */ + disableLayouts : function(isDisabled) { + config.enabled = (isDisabled !== true); + } + }; + }) + + .directive('mdLayoutCss' , disableLayoutDirective ) + .directive('ngCloak' , buildCloakInterceptor('ng-cloak')) + + .directive('layoutWrap' , attributeWithoutValue('layout-wrap')) + .directive('layoutNowrap' , attributeWithoutValue('layout-nowrap')) + .directive('layoutNoWrap' , attributeWithoutValue('layout-no-wrap')) + .directive('layoutFill' , attributeWithoutValue('layout-fill')) + + // !! Deprecated attributes: use the `-lt` (aka less-than) notations + + .directive('layoutLtMd' , warnAttrNotSupported('layout-lt-md', true)) + .directive('layoutLtLg' , warnAttrNotSupported('layout-lt-lg', true)) + .directive('flexLtMd' , warnAttrNotSupported('flex-lt-md', true)) + .directive('flexLtLg' , warnAttrNotSupported('flex-lt-lg', true)) + + .directive('layoutAlignLtMd', warnAttrNotSupported('layout-align-lt-md')) + .directive('layoutAlignLtLg', warnAttrNotSupported('layout-align-lt-lg')) + .directive('flexOrderLtMd' , warnAttrNotSupported('flex-order-lt-md')) + .directive('flexOrderLtLg' , warnAttrNotSupported('flex-order-lt-lg')) + .directive('offsetLtMd' , warnAttrNotSupported('flex-offset-lt-md')) + .directive('offsetLtLg' , warnAttrNotSupported('flex-offset-lt-lg')) + + .directive('hideLtMd' , warnAttrNotSupported('hide-lt-md')) + .directive('hideLtLg' , warnAttrNotSupported('hide-lt-lg')) + .directive('showLtMd' , warnAttrNotSupported('show-lt-md')) + .directive('showLtLg' , warnAttrNotSupported('show-lt-lg')) + + // Determine if + .config( detectDisabledLayouts ); + + /** + * Converts snake_case to camelCase. + * Also there is special case for Moz prefix starting with upper case letter. + * @param name Name to normalize + */ + function directiveNormalize(name) { + return name + .replace(PREFIX_REGEXP, '') + .replace(SPECIAL_CHARS_REGEXP, function(_, separator, letter, offset) { + return offset ? letter.toUpperCase() : letter; + }); + } + + } + + + /** + * Detect if any of the HTML tags has a [md-layouts-disabled] attribute; + * If yes, then immediately disable all layout API features + * + * Note: this attribute should be specified on either the HTML or BODY tags + */ + /** + * @ngInject + */ + function detectDisabledLayouts() { + var isDisabled = !!document.querySelector('[md-layouts-disabled]'); + config.enabled = !isDisabled; + } + + /** + * Special directive that will disable ALL Layout conversions of layout + * attribute(s) to classname(s). + * + * + * + * + * + * ... + * + * + * Note: Using md-layout-css directive requires the developer to load the Material + * Layout Attribute stylesheet (which only uses attribute selectors): + * + * `angular-material.layout.css` + * + * Another option is to use the LayoutProvider to configure and disable the attribute + * conversions; this would obviate the use of the `md-layout-css` directive + * + */ + function disableLayoutDirective() { + // Return a 1x-only, first-match attribute directive + config.enabled = false; + + return { + restrict : 'A', + priority : '900' + }; + } + + /** + * Tail-hook ngCloak to delay the uncloaking while Layout transformers + * finish processing. Eliminates flicker with Material.Layouts + */ + function buildCloakInterceptor(className) { + return [ '$timeout', function($timeout){ + return { + restrict : 'A', + priority : -10, // run after normal ng-cloak + compile : function( element ) { + if (!config.enabled) return angular.noop; + + // Re-add the cloak + element.addClass(className); + + return function( scope, element ) { + // Wait while layout injectors configure, then uncloak + // NOTE: $rAF does not delay enough... and this is a 1x-only event, + // $timeout is acceptable. + $timeout( function(){ + element.removeClass(className); + }, 10, false); + }; + } + }; + }]; + } + + + // ********************************************************************************* + // + // These functions create registration functions for ngMaterial Layout attribute directives + // This provides easy translation to switch ngMaterial attribute selectors to + // CLASS selectors and directives; which has huge performance implications + // for IE Browsers + // + // ********************************************************************************* + + /** + * Creates a directive registration function where a possible dynamic attribute + * value will be observed/watched. + * @param {string} className attribute name; eg `layout-gt-md` with value ="row" + */ + function attributeWithObserve(className) { + + return ['$mdUtil', '$interpolate', "$log", function(_$mdUtil_, _$interpolate_, _$log_) { + $mdUtil = _$mdUtil_; + $interpolate = _$interpolate_; + $log = _$log_; + + return { + restrict: 'A', + compile: function(element, attr) { + var linkFn; + if (config.enabled) { + // immediately replace static (non-interpolated) invalid values... + + validateAttributeUsage(className, attr, element, $log); + + validateAttributeValue( className, + getNormalizedAttrValue(className, attr, ""), + buildUpdateFn(element, className, attr) + ); + + linkFn = translateWithValueToCssClass; + } + + // Use for postLink to account for transforms after ng-transclude. + return linkFn || angular.noop; + } + }; + }]; + + /** + * Add as transformed class selector(s), then + * remove the deprecated attribute selector + */ + function translateWithValueToCssClass(scope, element, attrs) { + var updateFn = updateClassWithValue(element, className, attrs); + var unwatch = attrs.$observe(attrs.$normalize(className), updateFn); + + updateFn(getNormalizedAttrValue(className, attrs, "")); + scope.$on("$destroy", function() { unwatch(); }); + } + } + + /** + * Creates a registration function for ngMaterial Layout attribute directive. + * This is a `simple` transpose of attribute usage to class usage; where we ignore + * any attribute value + */ + function attributeWithoutValue(className) { + return ['$mdUtil', '$interpolate', "$log", function(_$mdUtil_, _$interpolate_, _$log_) { + $mdUtil = _$mdUtil_; + $interpolate = _$interpolate_; + $log = _$log_; + + return { + restrict: 'A', + compile: function(element, attr) { + var linkFn; + if (config.enabled) { + // immediately replace static (non-interpolated) invalid values... + + validateAttributeValue( className, + getNormalizedAttrValue(className, attr, ""), + buildUpdateFn(element, className, attr) + ); + + translateToCssClass(null, element); + + // Use for postLink to account for transforms after ng-transclude. + linkFn = translateToCssClass; + } + + return linkFn || angular.noop; + } + }; + }]; + + /** + * Add as transformed class selector, then + * remove the deprecated attribute selector + */ + function translateToCssClass(scope, element) { + element.addClass(className); + } + } + + + + /** + * After link-phase, do NOT remove deprecated layout attribute selector. + * Instead watch the attribute so interpolated data-bindings to layout + * selectors will continue to be supported. + * + * $observe() the className and update with new class (after removing the last one) + * + * e.g. `layout="{{layoutDemo.direction}}"` will update... + * + * NOTE: The value must match one of the specified styles in the CSS. + * For example `flex-gt-md="{{size}}` where `scope.size == 47` will NOT work since + * only breakpoints for 0, 5, 10, 15... 100, 33, 34, 66, 67 are defined. + * + */ + function updateClassWithValue(element, className) { + var lastClass; + + return function updateClassFn(newValue) { + var value = validateAttributeValue(className, newValue || ""); + if ( angular.isDefined(value) ) { + if (lastClass) element.removeClass(lastClass); + lastClass = !value ? className : className + "-" + value.replace(WHITESPACE, "-"); + element.addClass(lastClass); + } + }; + } + + /** + * Provide console warning that this layout attribute has been deprecated + * + */ + function warnAttrNotSupported(className) { + var parts = className.split("-"); + return ["$log", function($log) { + $log.warn(className + "has been deprecated. Please use a `" + parts[0] + "-gt-` variant."); + return angular.noop; + }]; + } + + /** + * Centralize warnings for known flexbox issues (especially IE-related issues) + */ + function validateAttributeUsage(className, attr, element, $log){ + var message, usage, url; + var nodeName = element[0].nodeName.toLowerCase(); + + switch(className.replace(SUFFIXES,"")) { + case "flex": + if ((nodeName == "md-button") || (nodeName == "fieldset")){ + // @see https://github.com/philipwalton/flexbugs#9-some-html-elements-cant-be-flex-containers + // Use
      wrapper inside (preferred) or outside + + usage = "<" + nodeName + " " + className + ">"; + url = "https://github.com/philipwalton/flexbugs#9-some-html-elements-cant-be-flex-containers"; + message = "Markup '{0}' may not work as expected in IE Browsers. Consult '{1}' for details."; + + $log.warn( $mdUtil.supplant(message, [usage, url]) ); + } + } + + } + + + /** + * For the Layout attribute value, validate or replace with default + * fallback value + */ + function validateAttributeValue(className, value, updateFn) { + var origValue = value; + + if (!needsInterpolation(value)) { + switch (className.replace(SUFFIXES,"")) { + case 'layout' : + if ( !findIn(value, LAYOUT_OPTIONS) ) { + value = LAYOUT_OPTIONS[0]; // 'row'; + } + break; + + case 'flex' : + if (!findIn(value, FLEX_OPTIONS)) { + if (isNaN(value)) { + value = ''; + } + } + break; + + case 'flex-offset' : + case 'flex-order' : + if (!value || isNaN(+value)) { + value = '0'; + } + break; + + case 'layout-align' : + var axis = extractAlignAxis(value); + value = $mdUtil.supplant("{main}-{cross}",axis); + break; + + case 'layout-padding' : + case 'layout-margin' : + case 'layout-fill' : + case 'layout-wrap' : + case 'layout-nowrap' : + case 'layout-nowrap' : + value = ''; + break; + } + + if (value != origValue) { + (updateFn || angular.noop)(value); + } + } + + return value; + } + + /** + * Replace current attribute value with fallback value + */ + function buildUpdateFn(element, className, attrs) { + return function updateAttrValue(fallback) { + if (!needsInterpolation(fallback)) { + // Do not modify the element's attribute value; so + // uses '' will not + // be affected. Just update the attrs value. + attrs[attrs.$normalize(className)] = fallback; + } + }; + } + + /** + * See if the original value has interpolation symbols: + * e.g. flex-gt-md="{{triggerPoint}}" + */ + function needsInterpolation(value) { + return (value || "").indexOf($interpolate.startSymbol()) > -1; + } + + function getNormalizedAttrValue(className, attrs, defaultVal) { + var normalizedAttr = attrs.$normalize(className); + return attrs[normalizedAttr] ? attrs[normalizedAttr].replace(WHITESPACE, "-") : defaultVal || null; + } + + function findIn(item, list, replaceWith) { + item = replaceWith && item ? item.replace(WHITESPACE, replaceWith) : item; + + var found = false; + if (item) { + list.forEach(function(it) { + it = replaceWith ? it.replace(WHITESPACE, replaceWith) : it; + found = found || (it === item); + }); + } + return found; + } + + function extractAlignAxis(attrValue) { + var axis = { + main : "start", + cross: "stretch" + }, values; + + attrValue = (attrValue || ""); + + if ( attrValue.indexOf("-") === 0 || attrValue.indexOf(" ") === 0) { + // For missing main-axis values + attrValue = "none" + attrValue; + } + + values = attrValue.toLowerCase().trim().replace(WHITESPACE, "-").split("-"); + if ( values.length && (values[0] === "space") ) { + // for main-axis values of "space-around" or "space-between" + values = [ values[0]+"-"+values[1],values[2] ]; + } + + if ( values.length > 0 ) axis.main = values[0] || axis.main; + if ( values.length > 1 ) axis.cross = values[1] || axis.cross; + + if ( ALIGNMENT_MAIN_AXIS.indexOf(axis.main) < 0 ) axis.main = "start"; + if ( ALIGNMENT_CROSS_AXIS.indexOf(axis.cross) < 0 ) axis.cross = "stretch"; + + return axis; + } + + +})(); + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc service + * @name $$mdMeta + * @module material.core.meta + * + * @description + * + * A provider and a service that simplifies meta tags access + * + * Note: This is intended only for use with dynamic meta tags such as browser color and title. + * Tags that are only processed when the page is rendered (such as `charset`, and `http-equiv`) + * will not work since `$$mdMeta` adds the tags after the page has already been loaded. + * + * ```js + * app.config(function($$mdMetaProvider) { + * var removeMeta = $$mdMetaProvider.setMeta('meta-name', 'content'); + * var metaValue = $$mdMetaProvider.getMeta('meta-name'); // -> 'content' + * + * removeMeta(); + * }); + * + * app.controller('myController', function($$mdMeta) { + * var removeMeta = $$mdMeta.setMeta('meta-name', 'content'); + * var metaValue = $$mdMeta.getMeta('meta-name'); // -> 'content' + * + * removeMeta(); + * }); + * ``` + * + * @returns {$$mdMeta.$service} + * + */ +angular.module('material.core.meta', []) + .provider('$$mdMeta', function () { + var head = angular.element(document.head); + var metaElements = {}; + + /** + * Checks if the requested element was written manually and maps it + * + * @param {string} name meta tag 'name' attribute value + * @returns {boolean} returns true if there is an element with the requested name + */ + function mapExistingElement(name) { + if (metaElements[name]) { + return true; + } + + var element = document.getElementsByName(name)[0]; + + if (!element) { + return false; + } + + metaElements[name] = angular.element(element); + + return true; + } + + /** + * @ngdoc method + * @name $$mdMeta#setMeta + * + * @description + * Creates meta element with the 'name' and 'content' attributes, + * if the meta tag is already created than we replace the 'content' value + * + * @param {string} name meta tag 'name' attribute value + * @param {string} content meta tag 'content' attribute value + * @returns {function} remove function + * + */ + function setMeta(name, content) { + mapExistingElement(name); + + if (!metaElements[name]) { + var newMeta = angular.element(''); + head.append(newMeta); + metaElements[name] = newMeta; + } + else { + metaElements[name].attr('content', content); + } + + return function () { + metaElements[name].attr('content', ''); + metaElements[name].remove(); + delete metaElements[name]; + }; + } + + /** + * @ngdoc method + * @name $$mdMeta#getMeta + * + * @description + * Gets the 'content' attribute value of the wanted meta element + * + * @param {string} name meta tag 'name' attribute value + * @returns {string} content attribute value + */ + function getMeta(name) { + if (!mapExistingElement(name)) { + throw Error('$$mdMeta: could not find a meta tag with the name \'' + name + '\''); + } + + return metaElements[name].attr('content'); + } + + var module = { + setMeta: setMeta, + getMeta: getMeta + }; + + return angular.extend({}, module, { + $get: function () { + return module; + } + }); + }); +})(); +(function(){ +"use strict"; + + /** + * @ngdoc module + * @name material.core.componentRegistry + * + * @description + * A component instance registration service. + * Note: currently this as a private service in the SideNav component. + */ + ComponentRegistry.$inject = ["$log", "$q"]; + angular.module('material.core') + .factory('$mdComponentRegistry', ComponentRegistry); + + /* + * @private + * @ngdoc factory + * @name ComponentRegistry + * @module material.core.componentRegistry + * + */ + function ComponentRegistry($log, $q) { + + var self; + var instances = [ ]; + var pendings = { }; + + return self = { + /** + * Used to print an error when an instance for a handle isn't found. + */ + notFoundError: function(handle, msgContext) { + $log.error( (msgContext || "") + 'No instance found for handle', handle); + }, + /** + * Return all registered instances as an array. + */ + getInstances: function() { + return instances; + }, + + /** + * Get a registered instance. + * @param handle the String handle to look up for a registered instance. + */ + get: function(handle) { + if ( !isValidID(handle) ) return null; + + var i, j, instance; + for(i = 0, j = instances.length; i < j; i++) { + instance = instances[i]; + if(instance.$$mdHandle === handle) { + return instance; + } + } + return null; + }, + + /** + * Register an instance. + * @param instance the instance to register + * @param handle the handle to identify the instance under. + */ + register: function(instance, handle) { + if ( !handle ) return angular.noop; + + instance.$$mdHandle = handle; + instances.push(instance); + resolveWhen(); + + return deregister; + + /** + * Remove registration for an instance + */ + function deregister() { + var index = instances.indexOf(instance); + if (index !== -1) { + instances.splice(index, 1); + } + } + + /** + * Resolve any pending promises for this instance + */ + function resolveWhen() { + var dfd = pendings[handle]; + if ( dfd ) { + dfd.forEach(function (promise) { + promise.resolve(instance); + }); + delete pendings[handle]; + } + } + }, + + /** + * Async accessor to registered component instance + * If not available then a promise is created to notify + * all listeners when the instance is registered. + */ + when : function(handle) { + if ( isValidID(handle) ) { + var deferred = $q.defer(); + var instance = self.get(handle); + + if ( instance ) { + deferred.resolve( instance ); + } else { + if (pendings[handle] === undefined) { + pendings[handle] = []; + } + pendings[handle].push(deferred); + } + + return deferred.promise; + } + return $q.reject("Invalid `md-component-id` value."); + } + + }; + + function isValidID(handle){ + return handle && (handle !== ""); + } + + } + +})(); +(function(){ +"use strict"; + +(function() { + 'use strict'; + + /** + * @ngdoc service + * @name $mdButtonInkRipple + * @module material.core + * + * @description + * Provides ripple effects for md-button. See $mdInkRipple service for all possible configuration options. + * + * @param {object=} scope Scope within the current context + * @param {object=} element The element the ripple effect should be applied to + * @param {object=} options (Optional) Configuration options to override the default ripple configuration + */ + + MdButtonInkRipple.$inject = ["$mdInkRipple"]; + angular.module('material.core') + .factory('$mdButtonInkRipple', MdButtonInkRipple); + + function MdButtonInkRipple($mdInkRipple) { + return { + attach: function attachRipple(scope, element, options) { + options = angular.extend(optionsForElement(element), options); + + return $mdInkRipple.attach(scope, element, options); + } + }; + + function optionsForElement(element) { + if (element.hasClass('md-icon-button')) { + return { + isMenuItem: element.hasClass('md-menu-item'), + fitRipple: true, + center: true + }; + } else { + return { + isMenuItem: element.hasClass('md-menu-item'), + dimBackground: true + } + } + }; + }; +})(); + +})(); +(function(){ +"use strict"; + +(function() { + 'use strict'; + + /** + * @ngdoc service + * @name $mdCheckboxInkRipple + * @module material.core + * + * @description + * Provides ripple effects for md-checkbox. See $mdInkRipple service for all possible configuration options. + * + * @param {object=} scope Scope within the current context + * @param {object=} element The element the ripple effect should be applied to + * @param {object=} options (Optional) Configuration options to override the defaultripple configuration + */ + + MdCheckboxInkRipple.$inject = ["$mdInkRipple"]; + angular.module('material.core') + .factory('$mdCheckboxInkRipple', MdCheckboxInkRipple); + + function MdCheckboxInkRipple($mdInkRipple) { + return { + attach: attach + }; + + function attach(scope, element, options) { + return $mdInkRipple.attach(scope, element, angular.extend({ + center: true, + dimBackground: false, + fitRipple: true + }, options)); + }; + }; +})(); + +})(); +(function(){ +"use strict"; + +(function() { + 'use strict'; + + /** + * @ngdoc service + * @name $mdListInkRipple + * @module material.core + * + * @description + * Provides ripple effects for md-list. See $mdInkRipple service for all possible configuration options. + * + * @param {object=} scope Scope within the current context + * @param {object=} element The element the ripple effect should be applied to + * @param {object=} options (Optional) Configuration options to override the defaultripple configuration + */ + + MdListInkRipple.$inject = ["$mdInkRipple"]; + angular.module('material.core') + .factory('$mdListInkRipple', MdListInkRipple); + + function MdListInkRipple($mdInkRipple) { + return { + attach: attach + }; + + function attach(scope, element, options) { + return $mdInkRipple.attach(scope, element, angular.extend({ + center: false, + dimBackground: true, + outline: false, + rippleSize: 'full' + }, options)); + }; + }; +})(); + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.core.ripple + * @description + * Ripple + */ +InkRippleCtrl.$inject = ["$scope", "$element", "rippleOptions", "$window", "$timeout", "$mdUtil", "$mdColorUtil"]; +InkRippleDirective.$inject = ["$mdButtonInkRipple", "$mdCheckboxInkRipple"]; +angular.module('material.core') + .provider('$mdInkRipple', InkRippleProvider) + .directive('mdInkRipple', InkRippleDirective) + .directive('mdNoInk', attrNoDirective) + .directive('mdNoBar', attrNoDirective) + .directive('mdNoStretch', attrNoDirective); + +var DURATION = 450; + +/** + * @ngdoc directive + * @name mdInkRipple + * @module material.core.ripple + * + * @description + * The `md-ink-ripple` directive allows you to specify the ripple color or id a ripple is allowed. + * + * @param {string|boolean} md-ink-ripple A color string `#FF0000` or boolean (`false` or `0`) for preventing ripple + * + * @usage + * ### String values + * + * + * Ripples in red + * + * + * + * Not rippling + * + * + * + * ### Interpolated values + * + * + * Ripples with the return value of 'randomColor' function + * + * + * + * Ripples if 'canRipple' function return value is not 'false' or '0' + * + * + */ +function InkRippleDirective ($mdButtonInkRipple, $mdCheckboxInkRipple) { + return { + controller: angular.noop, + link: function (scope, element, attr) { + attr.hasOwnProperty('mdInkRippleCheckbox') + ? $mdCheckboxInkRipple.attach(scope, element) + : $mdButtonInkRipple.attach(scope, element); + } + }; +} + +/** + * @ngdoc service + * @name $mdInkRipple + * @module material.core.ripple + * + * @description + * `$mdInkRipple` is a service for adding ripples to any element + * + * @usage + * + * app.factory('$myElementInkRipple', function($mdInkRipple) { + * return { + * attach: function (scope, element, options) { + * return $mdInkRipple.attach(scope, element, angular.extend({ + * center: false, + * dimBackground: true + * }, options)); + * } + * }; + * }); + * + * app.controller('myController', function ($scope, $element, $myElementInkRipple) { + * $scope.onClick = function (ev) { + * $myElementInkRipple.attach($scope, angular.element(ev.target), { center: true }); + * } + * }); + * + * + * ### Disabling ripples globally + * If you want to disable ink ripples globally, for all components, you can call the + * `disableInkRipple` method in your app's config. + * + * + * app.config(function ($mdInkRippleProvider) { + * $mdInkRippleProvider.disableInkRipple(); + * }); + */ + +function InkRippleProvider () { + var isDisabledGlobally = false; + + return { + disableInkRipple: disableInkRipple, + $get: ["$injector", function($injector) { + return { attach: attach }; + + /** + * @ngdoc method + * @name $mdInkRipple#attach + * + * @description + * Attaching given scope, element and options to inkRipple controller + * + * @param {object=} scope Scope within the current context + * @param {object=} element The element the ripple effect should be applied to + * @param {object=} options (Optional) Configuration options to override the defaultRipple configuration + * * `center` - Whether the ripple should start from the center of the container element + * * `dimBackground` - Whether the background should be dimmed with the ripple color + * * `colorElement` - The element the ripple should take its color from, defined by css property `color` + * * `fitRipple` - Whether the ripple should fill the element + */ + function attach (scope, element, options) { + if (isDisabledGlobally || element.controller('mdNoInk')) return angular.noop; + return $injector.instantiate(InkRippleCtrl, { + $scope: scope, + $element: element, + rippleOptions: options + }); + } + }] + }; + + /** + * @ngdoc method + * @name $mdInkRipple#disableInkRipple + * + * @description + * A config-time method that, when called, disables ripples globally. + */ + function disableInkRipple () { + isDisabledGlobally = true; + } +} + +/** + * Controller used by the ripple service in order to apply ripples + * @ngInject + */ +function InkRippleCtrl ($scope, $element, rippleOptions, $window, $timeout, $mdUtil, $mdColorUtil) { + this.$window = $window; + this.$timeout = $timeout; + this.$mdUtil = $mdUtil; + this.$mdColorUtil = $mdColorUtil; + this.$scope = $scope; + this.$element = $element; + this.options = rippleOptions; + this.mousedown = false; + this.ripples = []; + this.timeout = null; // Stores a reference to the most-recent ripple timeout + this.lastRipple = null; + + $mdUtil.valueOnUse(this, 'container', this.createContainer); + + this.$element.addClass('md-ink-ripple'); + + // attach method for unit tests + ($element.controller('mdInkRipple') || {}).createRipple = angular.bind(this, this.createRipple); + ($element.controller('mdInkRipple') || {}).setColor = angular.bind(this, this.color); + + this.bindEvents(); +} + + +/** + * Either remove or unlock any remaining ripples when the user mouses off of the element (either by + * mouseup or mouseleave event) + */ +function autoCleanup (self, cleanupFn) { + + if ( self.mousedown || self.lastRipple ) { + self.mousedown = false; + self.$mdUtil.nextTick( angular.bind(self, cleanupFn), false); + } + +} + + +/** + * Returns the color that the ripple should be (either based on CSS or hard-coded) + * @returns {string} + */ +InkRippleCtrl.prototype.color = function (value) { + var self = this; + + // If assigning a color value, apply it to background and the ripple color + if (angular.isDefined(value)) { + self._color = self._parseColor(value); + } + + // If color lookup, use assigned, defined, or inherited + return self._color || self._parseColor( self.inkRipple() ) || self._parseColor( getElementColor() ); + + /** + * Finds the color element and returns its text color for use as default ripple color + * @returns {string} + */ + function getElementColor () { + var items = self.options && self.options.colorElement ? self.options.colorElement : []; + var elem = items.length ? items[ 0 ] : self.$element[ 0 ]; + + return elem ? self.$window.getComputedStyle(elem).color : 'rgb(0,0,0)'; + } +}; + +/** + * Updating the ripple colors based on the current inkRipple value + * or the element's computed style color + */ +InkRippleCtrl.prototype.calculateColor = function () { + return this.color(); +}; + + +/** + * Takes a string color and converts it to RGBA format + * @param color {string} + * @param [multiplier] {int} + * @returns {string} + */ + +InkRippleCtrl.prototype._parseColor = function parseColor (color, multiplier) { + multiplier = multiplier || 1; + var colorUtil = this.$mdColorUtil; + + if (!color) return; + if (color.indexOf('rgba') === 0) return color.replace(/\d?\.?\d*\s*\)\s*$/, (0.1 * multiplier).toString() + ')'); + if (color.indexOf('rgb') === 0) return colorUtil.rgbToRgba(color); + if (color.indexOf('#') === 0) return colorUtil.hexToRgba(color); + +}; + +/** + * Binds events to the root element for + */ +InkRippleCtrl.prototype.bindEvents = function () { + this.$element.on('mousedown', angular.bind(this, this.handleMousedown)); + this.$element.on('mouseup touchend', angular.bind(this, this.handleMouseup)); + this.$element.on('mouseleave', angular.bind(this, this.handleMouseup)); + this.$element.on('touchmove', angular.bind(this, this.handleTouchmove)); +}; + +/** + * Create a new ripple on every mousedown event from the root element + * @param event {MouseEvent} + */ +InkRippleCtrl.prototype.handleMousedown = function (event) { + if ( this.mousedown ) return; + + // When jQuery is loaded, we have to get the original event + if (event.hasOwnProperty('originalEvent')) event = event.originalEvent; + this.mousedown = true; + if (this.options.center) { + this.createRipple(this.container.prop('clientWidth') / 2, this.container.prop('clientWidth') / 2); + } else { + + // We need to calculate the relative coordinates if the target is a sublayer of the ripple element + if (event.srcElement !== this.$element[0]) { + var layerRect = this.$element[0].getBoundingClientRect(); + var layerX = event.clientX - layerRect.left; + var layerY = event.clientY - layerRect.top; + + this.createRipple(layerX, layerY); + } else { + this.createRipple(event.offsetX, event.offsetY); + } + } +}; + +/** + * Either remove or unlock any remaining ripples when the user mouses off of the element (either by + * mouseup, touchend or mouseleave event) + */ +InkRippleCtrl.prototype.handleMouseup = function () { + autoCleanup(this, this.clearRipples); +}; + +/** + * Either remove or unlock any remaining ripples when the user mouses off of the element (by + * touchmove) + */ +InkRippleCtrl.prototype.handleTouchmove = function () { + autoCleanup(this, this.deleteRipples); +}; + +/** + * Cycles through all ripples and attempts to remove them. + */ +InkRippleCtrl.prototype.deleteRipples = function () { + for (var i = 0; i < this.ripples.length; i++) { + this.ripples[ i ].remove(); + } +}; + +/** + * Cycles through all ripples and attempts to remove them with fade. + * Depending on logic within `fadeInComplete`, some removals will be postponed. + */ +InkRippleCtrl.prototype.clearRipples = function () { + for (var i = 0; i < this.ripples.length; i++) { + this.fadeInComplete(this.ripples[ i ]); + } +}; + +/** + * Creates the ripple container element + * @returns {*} + */ +InkRippleCtrl.prototype.createContainer = function () { + var container = angular.element('
      '); + this.$element.append(container); + return container; +}; + +InkRippleCtrl.prototype.clearTimeout = function () { + if (this.timeout) { + this.$timeout.cancel(this.timeout); + this.timeout = null; + } +}; + +InkRippleCtrl.prototype.isRippleAllowed = function () { + var element = this.$element[0]; + do { + if (!element.tagName || element.tagName === 'BODY') break; + + if (element && angular.isFunction(element.hasAttribute)) { + if (element.hasAttribute('disabled')) return false; + if (this.inkRipple() === 'false' || this.inkRipple() === '0') return false; + } + + } while (element = element.parentNode); + return true; +}; + +/** + * The attribute `md-ink-ripple` may be a static or interpolated + * color value OR a boolean indicator (used to disable ripples) + */ +InkRippleCtrl.prototype.inkRipple = function () { + return this.$element.attr('md-ink-ripple'); +}; + +/** + * Creates a new ripple and adds it to the container. Also tracks ripple in `this.ripples`. + * @param left + * @param top + */ +InkRippleCtrl.prototype.createRipple = function (left, top) { + if (!this.isRippleAllowed()) return; + + var ctrl = this; + var colorUtil = ctrl.$mdColorUtil; + var ripple = angular.element('
      '); + var width = this.$element.prop('clientWidth'); + var height = this.$element.prop('clientHeight'); + var x = Math.max(Math.abs(width - left), left) * 2; + var y = Math.max(Math.abs(height - top), top) * 2; + var size = getSize(this.options.fitRipple, x, y); + var color = this.calculateColor(); + + ripple.css({ + left: left + 'px', + top: top + 'px', + background: 'black', + width: size + 'px', + height: size + 'px', + backgroundColor: colorUtil.rgbaToRgb(color), + borderColor: colorUtil.rgbaToRgb(color) + }); + this.lastRipple = ripple; + + // we only want one timeout to be running at a time + this.clearTimeout(); + this.timeout = this.$timeout(function () { + ctrl.clearTimeout(); + if (!ctrl.mousedown) ctrl.fadeInComplete(ripple); + }, DURATION * 0.35, false); + + if (this.options.dimBackground) this.container.css({ backgroundColor: color }); + this.container.append(ripple); + this.ripples.push(ripple); + ripple.addClass('md-ripple-placed'); + + this.$mdUtil.nextTick(function () { + + ripple.addClass('md-ripple-scaled md-ripple-active'); + ctrl.$timeout(function () { + ctrl.clearRipples(); + }, DURATION, false); + + }, false); + + function getSize (fit, x, y) { + return fit + ? Math.max(x, y) + : Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)); + } +}; + + + +/** + * After fadeIn finishes, either kicks off the fade-out animation or queues the element for removal on mouseup + * @param ripple + */ +InkRippleCtrl.prototype.fadeInComplete = function (ripple) { + if (this.lastRipple === ripple) { + if (!this.timeout && !this.mousedown) { + this.removeRipple(ripple); + } + } else { + this.removeRipple(ripple); + } +}; + +/** + * Kicks off the animation for removing a ripple + * @param ripple {Element} + */ +InkRippleCtrl.prototype.removeRipple = function (ripple) { + var ctrl = this; + var index = this.ripples.indexOf(ripple); + if (index < 0) return; + this.ripples.splice(this.ripples.indexOf(ripple), 1); + ripple.removeClass('md-ripple-active'); + ripple.addClass('md-ripple-remove'); + if (this.ripples.length === 0) this.container.css({ backgroundColor: '' }); + // use a 2-second timeout in order to allow for the animation to finish + // we don't actually care how long the animation takes + this.$timeout(function () { + ctrl.fadeOutComplete(ripple); + }, DURATION, false); +}; + +/** + * Removes the provided ripple from the DOM + * @param ripple + */ +InkRippleCtrl.prototype.fadeOutComplete = function (ripple) { + ripple.remove(); + this.lastRipple = null; +}; + +/** + * Used to create an empty directive. This is used to track flag-directives whose children may have + * functionality based on them. + * + * Example: `md-no-ink` will potentially be used by all child directives. + */ +function attrNoDirective () { + return { controller: angular.noop }; +} + +})(); +(function(){ +"use strict"; + +(function() { + 'use strict'; + + /** + * @ngdoc service + * @name $mdTabInkRipple + * @module material.core + * + * @description + * Provides ripple effects for md-tabs. See $mdInkRipple service for all possible configuration options. + * + * @param {object=} scope Scope within the current context + * @param {object=} element The element the ripple effect should be applied to + * @param {object=} options (Optional) Configuration options to override the defaultripple configuration + */ + + MdTabInkRipple.$inject = ["$mdInkRipple"]; + angular.module('material.core') + .factory('$mdTabInkRipple', MdTabInkRipple); + + function MdTabInkRipple($mdInkRipple) { + return { + attach: attach + }; + + function attach(scope, element, options) { + return $mdInkRipple.attach(scope, element, angular.extend({ + center: false, + dimBackground: true, + outline: false, + rippleSize: 'full' + }, options)); + }; + }; +})(); + +})(); +(function(){ +"use strict"; + +angular.module('material.core.theming.palette', []) +.constant('$mdColorPalette', { + 'red': { + '50': '#ffebee', + '100': '#ffcdd2', + '200': '#ef9a9a', + '300': '#e57373', + '400': '#ef5350', + '500': '#f44336', + '600': '#e53935', + '700': '#d32f2f', + '800': '#c62828', + '900': '#b71c1c', + 'A100': '#ff8a80', + 'A200': '#ff5252', + 'A400': '#ff1744', + 'A700': '#d50000', + 'contrastDefaultColor': 'light', + 'contrastDarkColors': '50 100 200 300 A100', + 'contrastStrongLightColors': '400 500 600 700 A200 A400 A700' + }, + 'pink': { + '50': '#fce4ec', + '100': '#f8bbd0', + '200': '#f48fb1', + '300': '#f06292', + '400': '#ec407a', + '500': '#e91e63', + '600': '#d81b60', + '700': '#c2185b', + '800': '#ad1457', + '900': '#880e4f', + 'A100': '#ff80ab', + 'A200': '#ff4081', + 'A400': '#f50057', + 'A700': '#c51162', + 'contrastDefaultColor': 'light', + 'contrastDarkColors': '50 100 200 A100', + 'contrastStrongLightColors': '500 600 A200 A400 A700' + }, + 'purple': { + '50': '#f3e5f5', + '100': '#e1bee7', + '200': '#ce93d8', + '300': '#ba68c8', + '400': '#ab47bc', + '500': '#9c27b0', + '600': '#8e24aa', + '700': '#7b1fa2', + '800': '#6a1b9a', + '900': '#4a148c', + 'A100': '#ea80fc', + 'A200': '#e040fb', + 'A400': '#d500f9', + 'A700': '#aa00ff', + 'contrastDefaultColor': 'light', + 'contrastDarkColors': '50 100 200 A100', + 'contrastStrongLightColors': '300 400 A200 A400 A700' + }, + 'deep-purple': { + '50': '#ede7f6', + '100': '#d1c4e9', + '200': '#b39ddb', + '300': '#9575cd', + '400': '#7e57c2', + '500': '#673ab7', + '600': '#5e35b1', + '700': '#512da8', + '800': '#4527a0', + '900': '#311b92', + 'A100': '#b388ff', + 'A200': '#7c4dff', + 'A400': '#651fff', + 'A700': '#6200ea', + 'contrastDefaultColor': 'light', + 'contrastDarkColors': '50 100 200 A100', + 'contrastStrongLightColors': '300 400 A200' + }, + 'indigo': { + '50': '#e8eaf6', + '100': '#c5cae9', + '200': '#9fa8da', + '300': '#7986cb', + '400': '#5c6bc0', + '500': '#3f51b5', + '600': '#3949ab', + '700': '#303f9f', + '800': '#283593', + '900': '#1a237e', + 'A100': '#8c9eff', + 'A200': '#536dfe', + 'A400': '#3d5afe', + 'A700': '#304ffe', + 'contrastDefaultColor': 'light', + 'contrastDarkColors': '50 100 200 A100', + 'contrastStrongLightColors': '300 400 A200 A400' + }, + 'blue': { + '50': '#e3f2fd', + '100': '#bbdefb', + '200': '#90caf9', + '300': '#64b5f6', + '400': '#42a5f5', + '500': '#2196f3', + '600': '#1e88e5', + '700': '#1976d2', + '800': '#1565c0', + '900': '#0d47a1', + 'A100': '#82b1ff', + 'A200': '#448aff', + 'A400': '#2979ff', + 'A700': '#2962ff', + 'contrastDefaultColor': 'light', + 'contrastDarkColors': '50 100 200 300 400 A100', + 'contrastStrongLightColors': '500 600 700 A200 A400 A700' + }, + 'light-blue': { + '50': '#e1f5fe', + '100': '#b3e5fc', + '200': '#81d4fa', + '300': '#4fc3f7', + '400': '#29b6f6', + '500': '#03a9f4', + '600': '#039be5', + '700': '#0288d1', + '800': '#0277bd', + '900': '#01579b', + 'A100': '#80d8ff', + 'A200': '#40c4ff', + 'A400': '#00b0ff', + 'A700': '#0091ea', + 'contrastDefaultColor': 'dark', + 'contrastLightColors': '600 700 800 900 A700', + 'contrastStrongLightColors': '600 700 800 A700' + }, + 'cyan': { + '50': '#e0f7fa', + '100': '#b2ebf2', + '200': '#80deea', + '300': '#4dd0e1', + '400': '#26c6da', + '500': '#00bcd4', + '600': '#00acc1', + '700': '#0097a7', + '800': '#00838f', + '900': '#006064', + 'A100': '#84ffff', + 'A200': '#18ffff', + 'A400': '#00e5ff', + 'A700': '#00b8d4', + 'contrastDefaultColor': 'dark', + 'contrastLightColors': '700 800 900', + 'contrastStrongLightColors': '700 800 900' + }, + 'teal': { + '50': '#e0f2f1', + '100': '#b2dfdb', + '200': '#80cbc4', + '300': '#4db6ac', + '400': '#26a69a', + '500': '#009688', + '600': '#00897b', + '700': '#00796b', + '800': '#00695c', + '900': '#004d40', + 'A100': '#a7ffeb', + 'A200': '#64ffda', + 'A400': '#1de9b6', + 'A700': '#00bfa5', + 'contrastDefaultColor': 'dark', + 'contrastLightColors': '500 600 700 800 900', + 'contrastStrongLightColors': '500 600 700' + }, + 'green': { + '50': '#e8f5e9', + '100': '#c8e6c9', + '200': '#a5d6a7', + '300': '#81c784', + '400': '#66bb6a', + '500': '#4caf50', + '600': '#43a047', + '700': '#388e3c', + '800': '#2e7d32', + '900': '#1b5e20', + 'A100': '#b9f6ca', + 'A200': '#69f0ae', + 'A400': '#00e676', + 'A700': '#00c853', + 'contrastDefaultColor': 'dark', + 'contrastLightColors': '500 600 700 800 900', + 'contrastStrongLightColors': '500 600 700' + }, + 'light-green': { + '50': '#f1f8e9', + '100': '#dcedc8', + '200': '#c5e1a5', + '300': '#aed581', + '400': '#9ccc65', + '500': '#8bc34a', + '600': '#7cb342', + '700': '#689f38', + '800': '#558b2f', + '900': '#33691e', + 'A100': '#ccff90', + 'A200': '#b2ff59', + 'A400': '#76ff03', + 'A700': '#64dd17', + 'contrastDefaultColor': 'dark', + 'contrastLightColors': '700 800 900', + 'contrastStrongLightColors': '700 800 900' + }, + 'lime': { + '50': '#f9fbe7', + '100': '#f0f4c3', + '200': '#e6ee9c', + '300': '#dce775', + '400': '#d4e157', + '500': '#cddc39', + '600': '#c0ca33', + '700': '#afb42b', + '800': '#9e9d24', + '900': '#827717', + 'A100': '#f4ff81', + 'A200': '#eeff41', + 'A400': '#c6ff00', + 'A700': '#aeea00', + 'contrastDefaultColor': 'dark', + 'contrastLightColors': '900', + 'contrastStrongLightColors': '900' + }, + 'yellow': { + '50': '#fffde7', + '100': '#fff9c4', + '200': '#fff59d', + '300': '#fff176', + '400': '#ffee58', + '500': '#ffeb3b', + '600': '#fdd835', + '700': '#fbc02d', + '800': '#f9a825', + '900': '#f57f17', + 'A100': '#ffff8d', + 'A200': '#ffff00', + 'A400': '#ffea00', + 'A700': '#ffd600', + 'contrastDefaultColor': 'dark' + }, + 'amber': { + '50': '#fff8e1', + '100': '#ffecb3', + '200': '#ffe082', + '300': '#ffd54f', + '400': '#ffca28', + '500': '#ffc107', + '600': '#ffb300', + '700': '#ffa000', + '800': '#ff8f00', + '900': '#ff6f00', + 'A100': '#ffe57f', + 'A200': '#ffd740', + 'A400': '#ffc400', + 'A700': '#ffab00', + 'contrastDefaultColor': 'dark' + }, + 'orange': { + '50': '#fff3e0', + '100': '#ffe0b2', + '200': '#ffcc80', + '300': '#ffb74d', + '400': '#ffa726', + '500': '#ff9800', + '600': '#fb8c00', + '700': '#f57c00', + '800': '#ef6c00', + '900': '#e65100', + 'A100': '#ffd180', + 'A200': '#ffab40', + 'A400': '#ff9100', + 'A700': '#ff6d00', + 'contrastDefaultColor': 'dark', + 'contrastLightColors': '800 900', + 'contrastStrongLightColors': '800 900' + }, + 'deep-orange': { + '50': '#fbe9e7', + '100': '#ffccbc', + '200': '#ffab91', + '300': '#ff8a65', + '400': '#ff7043', + '500': '#ff5722', + '600': '#f4511e', + '700': '#e64a19', + '800': '#d84315', + '900': '#bf360c', + 'A100': '#ff9e80', + 'A200': '#ff6e40', + 'A400': '#ff3d00', + 'A700': '#dd2c00', + 'contrastDefaultColor': 'light', + 'contrastDarkColors': '50 100 200 300 400 A100 A200', + 'contrastStrongLightColors': '500 600 700 800 900 A400 A700' + }, + 'brown': { + '50': '#efebe9', + '100': '#d7ccc8', + '200': '#bcaaa4', + '300': '#a1887f', + '400': '#8d6e63', + '500': '#795548', + '600': '#6d4c41', + '700': '#5d4037', + '800': '#4e342e', + '900': '#3e2723', + 'A100': '#d7ccc8', + 'A200': '#bcaaa4', + 'A400': '#8d6e63', + 'A700': '#5d4037', + 'contrastDefaultColor': 'light', + 'contrastDarkColors': '50 100 200 A100 A200', + 'contrastStrongLightColors': '300 400' + }, + 'grey': { + '50': '#fafafa', + '100': '#f5f5f5', + '200': '#eeeeee', + '300': '#e0e0e0', + '400': '#bdbdbd', + '500': '#9e9e9e', + '600': '#757575', + '700': '#616161', + '800': '#424242', + '900': '#212121', + 'A100': '#ffffff', + 'A200': '#000000', + 'A400': '#303030', + 'A700': '#616161', + 'contrastDefaultColor': 'dark', + 'contrastLightColors': '600 700 800 900 A200 A400 A700' + }, + 'blue-grey': { + '50': '#eceff1', + '100': '#cfd8dc', + '200': '#b0bec5', + '300': '#90a4ae', + '400': '#78909c', + '500': '#607d8b', + '600': '#546e7a', + '700': '#455a64', + '800': '#37474f', + '900': '#263238', + 'A100': '#cfd8dc', + 'A200': '#b0bec5', + 'A400': '#78909c', + 'A700': '#455a64', + 'contrastDefaultColor': 'light', + 'contrastDarkColors': '50 100 200 300 A100 A200', + 'contrastStrongLightColors': '400 500 700' + } +}); + +})(); +(function(){ +"use strict"; + +(function(angular) { + 'use strict'; +/** + * @ngdoc module + * @name material.core.theming + * @description + * Theming + */ +detectDisabledThemes.$inject = ["$mdThemingProvider"]; +ThemingDirective.$inject = ["$mdTheming", "$interpolate", "$log"]; +ThemableDirective.$inject = ["$mdTheming"]; +ThemingProvider.$inject = ["$mdColorPalette", "$$mdMetaProvider"]; +generateAllThemes.$inject = ["$injector", "$mdTheming"]; +angular.module('material.core.theming', ['material.core.theming.palette', 'material.core.meta']) + .directive('mdTheme', ThemingDirective) + .directive('mdThemable', ThemableDirective) + .directive('mdThemesDisabled', disableThemesDirective ) + .provider('$mdTheming', ThemingProvider) + .config( detectDisabledThemes ) + .run(generateAllThemes); + +/** + * Detect if the HTML or the BODY tags has a [md-themes-disabled] attribute + * If yes, then immediately disable all theme stylesheet generation and DOM injection + */ +/** + * @ngInject + */ +function detectDisabledThemes($mdThemingProvider) { + var isDisabled = !!document.querySelector('[md-themes-disabled]'); + $mdThemingProvider.disableTheming(isDisabled); +} + +/** + * @ngdoc service + * @name $mdThemingProvider + * @module material.core.theming + * + * @description Provider to configure the `$mdTheming` service. + * + * ### Default Theme + * The `$mdThemingProvider` uses by default the following theme configuration: + * + * - Primary Palette: `Primary` + * - Accent Palette: `Pink` + * - Warn Palette: `Deep-Orange` + * - Background Palette: `Grey` + * + * If you don't want to use the `md-theme` directive on the elements itself, you may want to overwrite + * the default theme.
      + * This can be done by using the following markup. + * + * + * myAppModule.config(function($mdThemingProvider) { + * $mdThemingProvider + * .theme('default') + * .primaryPalette('blue') + * .accentPalette('teal') + * .warnPalette('red') + * .backgroundPalette('grey'); + * }); + * + * + + * ### Dynamic Themes + * + * By default, if you change a theme at runtime, the `$mdTheming` service will not detect those changes.
      + * If you have an application, which changes its theme on runtime, you have to enable theme watching. + * + * + * myAppModule.config(function($mdThemingProvider) { + * // Enable theme watching. + * $mdThemingProvider.alwaysWatchTheme(true); + * }); + * + * + * ### Custom Theme Styles + * + * Sometimes you may want to use your own theme styles for some custom components.
      + * You are able to register your own styles by using the following markup. + * + * + * myAppModule.config(function($mdThemingProvider) { + * // Register our custom stylesheet into the theming provider. + * $mdThemingProvider.registerStyles(STYLESHEET); + * }); + * + * + * The `registerStyles` method only accepts strings as value, so you're actually not able to load an external + * stylesheet file into the `$mdThemingProvider`. + * + * If it's necessary to load an external stylesheet, we suggest using a bundler, which supports including raw content, + * like [raw-loader](https://github.com/webpack/raw-loader) for `webpack`. + * + * + * myAppModule.config(function($mdThemingProvider) { + * // Register your custom stylesheet into the theming provider. + * $mdThemingProvider.registerStyles(require('../styles/my-component.theme.css')); + * }); + * + * + * ### Browser color + * + * Enables browser header coloring + * for more info please visit: + * https://developers.google.com/web/fundamentals/design-and-ui/browser-customization/theme-color + * + * Options parameter:
      + * `theme` - A defined theme via `$mdThemeProvider` to use the palettes from. Default is `default` theme.
      + * `palette` - Can be any one of the basic material design palettes, extended defined palettes and 'primary', + * 'accent', 'background' and 'warn'. Default is `primary`.
      + * `hue` - The hue from the selected palette. Default is `800`
      + * + * + * myAppModule.config(function($mdThemingProvider) { + * // Enable browser color + * $mdThemingProvider.enableBrowserColor({ + * theme: 'myTheme', // Default is 'default' + * palette: 'accent', // Default is 'primary', any basic material palette and extended palettes are available + * hue: '200' // Default is '800' + * }); + * }); + * + */ + +/** + * @ngdoc method + * @name $mdThemingProvider#registerStyles + * @param {string} styles The styles to be appended to Angular Material's built in theme css. + */ +/** + * @ngdoc method + * @name $mdThemingProvider#setNonce + * @param {string} nonceValue The nonce to be added as an attribute to the theme style tags. + * Setting a value allows the use of CSP policy without using the unsafe-inline directive. + */ + +/** + * @ngdoc method + * @name $mdThemingProvider#setDefaultTheme + * @param {string} themeName Default theme name to be applied to elements. Default value is `default`. + */ + +/** + * @ngdoc method + * @name $mdThemingProvider#alwaysWatchTheme + * @param {boolean} watch Whether or not to always watch themes for changes and re-apply + * classes when they change. Default is `false`. Enabling can reduce performance. + */ + +/** + * @ngdoc method + * @name $mdThemingProvider#enableBrowserColor + * @param {Object=} options Options object for the browser color
      + * `theme` - A defined theme via `$mdThemeProvider` to use the palettes from. Default is `default` theme.
      + * `palette` - Can be any one of the basic material design palettes, extended defined palettes and 'primary', + * 'accent', 'background' and 'warn'. Default is `primary`.
      + * `hue` - The hue from the selected palette. Default is `800`
      + * @returns {Function} remove function of the browser color + */ + +/* Some Example Valid Theming Expressions + * ======================================= + * + * Intention group expansion: (valid for primary, accent, warn, background) + * + * {{primary-100}} - grab shade 100 from the primary palette + * {{primary-100-0.7}} - grab shade 100, apply opacity of 0.7 + * {{primary-100-contrast}} - grab shade 100's contrast color + * {{primary-hue-1}} - grab the shade assigned to hue-1 from the primary palette + * {{primary-hue-1-0.7}} - apply 0.7 opacity to primary-hue-1 + * {{primary-color}} - Generates .md-hue-1, .md-hue-2, .md-hue-3 with configured shades set for each hue + * {{primary-color-0.7}} - Apply 0.7 opacity to each of the above rules + * {{primary-contrast}} - Generates .md-hue-1, .md-hue-2, .md-hue-3 with configured contrast (ie. text) color shades set for each hue + * {{primary-contrast-0.7}} - Apply 0.7 opacity to each of the above rules + * + * Foreground expansion: Applies rgba to black/white foreground text + * + * {{foreground-1}} - used for primary text + * {{foreground-2}} - used for secondary text/divider + * {{foreground-3}} - used for disabled text + * {{foreground-4}} - used for dividers + * + */ + +// In memory generated CSS rules; registered by theme.name +var GENERATED = { }; + +// In memory storage of defined themes and color palettes (both loaded by CSS, and user specified) +var PALETTES; + +// Text Colors on light and dark backgrounds +// @see https://www.google.com/design/spec/style/color.html#color-text-background-colors +var DARK_FOREGROUND = { + name: 'dark', + '1': 'rgba(0,0,0,0.87)', + '2': 'rgba(0,0,0,0.54)', + '3': 'rgba(0,0,0,0.38)', + '4': 'rgba(0,0,0,0.12)' +}; +var LIGHT_FOREGROUND = { + name: 'light', + '1': 'rgba(255,255,255,1.0)', + '2': 'rgba(255,255,255,0.7)', + '3': 'rgba(255,255,255,0.5)', + '4': 'rgba(255,255,255,0.12)' +}; + +var DARK_SHADOW = '1px 1px 0px rgba(0,0,0,0.4), -1px -1px 0px rgba(0,0,0,0.4)'; +var LIGHT_SHADOW = ''; + +var DARK_CONTRAST_COLOR = colorToRgbaArray('rgba(0,0,0,0.87)'); +var LIGHT_CONTRAST_COLOR = colorToRgbaArray('rgba(255,255,255,0.87)'); +var STRONG_LIGHT_CONTRAST_COLOR = colorToRgbaArray('rgb(255,255,255)'); + +var THEME_COLOR_TYPES = ['primary', 'accent', 'warn', 'background']; +var DEFAULT_COLOR_TYPE = 'primary'; + +// A color in a theme will use these hues by default, if not specified by user. +var LIGHT_DEFAULT_HUES = { + 'accent': { + 'default': 'A200', + 'hue-1': 'A100', + 'hue-2': 'A400', + 'hue-3': 'A700' + }, + 'background': { + 'default': '50', + 'hue-1': 'A100', + 'hue-2': '100', + 'hue-3': '300' + } +}; + +var DARK_DEFAULT_HUES = { + 'background': { + 'default': 'A400', + 'hue-1': '800', + 'hue-2': '900', + 'hue-3': 'A200' + } +}; +THEME_COLOR_TYPES.forEach(function(colorType) { + // Color types with unspecified default hues will use these default hue values + var defaultDefaultHues = { + 'default': '500', + 'hue-1': '300', + 'hue-2': '800', + 'hue-3': 'A100' + }; + if (!LIGHT_DEFAULT_HUES[colorType]) LIGHT_DEFAULT_HUES[colorType] = defaultDefaultHues; + if (!DARK_DEFAULT_HUES[colorType]) DARK_DEFAULT_HUES[colorType] = defaultDefaultHues; +}); + +var VALID_HUE_VALUES = [ + '50', '100', '200', '300', '400', '500', '600', + '700', '800', '900', 'A100', 'A200', 'A400', 'A700' +]; + +var themeConfig = { + disableTheming : false, // Generate our themes at run time; also disable stylesheet DOM injection + generateOnDemand : false, // Whether or not themes are to be generated on-demand (vs. eagerly). + registeredStyles : [], // Custom styles registered to be used in the theming of custom components. + nonce : null // Nonce to be added as an attribute to the generated themes style tags. +}; + +/** + * + */ +function ThemingProvider($mdColorPalette, $$mdMetaProvider) { + ThemingService.$inject = ["$rootScope", "$log"]; + PALETTES = { }; + var THEMES = { }; + + var themingProvider; + + var alwaysWatchTheme = false; + var defaultTheme = 'default'; + + // Load JS Defined Palettes + angular.extend(PALETTES, $mdColorPalette); + + // Default theme defined in core.js + + /** + * Adds `theme-color` and `msapplication-navbutton-color` meta tags with the color parameter + * @param {string} color Hex value of the wanted browser color + * @returns {Function} Remove function of the meta tags + */ + var setBrowserColor = function (color) { + // Chrome, Firefox OS and Opera + var removeChrome = $$mdMetaProvider.setMeta('theme-color', color); + // Windows Phone + var removeWindows = $$mdMetaProvider.setMeta('msapplication-navbutton-color', color); + + return function () { + removeChrome(); + removeWindows(); + } + }; + + /** + * Enables browser header coloring + * for more info please visit: + * https://developers.google.com/web/fundamentals/design-and-ui/browser-customization/theme-color + * + * The default color is `800` from `primary` palette of the `default` theme + * + * options are: + * `theme` - A defined theme via `$mdThemeProvider` to use the palettes from. Default is `default` theme + * `palette` - Can be any one of the basic material design palettes, extended defined palettes and 'primary', + * 'accent', 'background' and 'warn'. Default is `primary` + * `hue` - The hue from the selected palette. Default is `800` + * + * @param {Object=} options Options object for the browser color + * @returns {Function} remove function of the browser color + */ + var enableBrowserColor = function (options) { + options = angular.isObject(options) ? options : {}; + + var theme = options.theme || 'default'; + var hue = options.hue || '800'; + + var palette = PALETTES[options.palette] || + PALETTES[THEMES[theme].colors[options.palette || 'primary'].name]; + + var color = angular.isObject(palette[hue]) ? palette[hue].hex : palette[hue]; + + return setBrowserColor(color); + }; + + return themingProvider = { + definePalette: definePalette, + extendPalette: extendPalette, + theme: registerTheme, + + /** + * return a read-only clone of the current theme configuration + */ + configuration : function() { + return angular.extend( { }, themeConfig, { + defaultTheme : defaultTheme, + alwaysWatchTheme : alwaysWatchTheme, + registeredStyles : [].concat(themeConfig.registeredStyles) + }); + }, + + /** + * Easy way to disable theming without having to use + * `.constant("$MD_THEME_CSS","");` This disables + * all dynamic theme style sheet generations and injections... + */ + disableTheming: function(isDisabled) { + themeConfig.disableTheming = angular.isUndefined(isDisabled) || !!isDisabled; + }, + + registerStyles: function(styles) { + themeConfig.registeredStyles.push(styles); + }, + + setNonce: function(nonceValue) { + themeConfig.nonce = nonceValue; + }, + + generateThemesOnDemand: function(onDemand) { + themeConfig.generateOnDemand = onDemand; + }, + + setDefaultTheme: function(theme) { + defaultTheme = theme; + }, + + alwaysWatchTheme: function(alwaysWatch) { + alwaysWatchTheme = alwaysWatch; + }, + + enableBrowserColor: enableBrowserColor, + + $get: ThemingService, + _LIGHT_DEFAULT_HUES: LIGHT_DEFAULT_HUES, + _DARK_DEFAULT_HUES: DARK_DEFAULT_HUES, + _PALETTES: PALETTES, + _THEMES: THEMES, + _parseRules: parseRules, + _rgba: rgba + }; + + // Example: $mdThemingProvider.definePalette('neonRed', { 50: '#f5fafa', ... }); + function definePalette(name, map) { + map = map || {}; + PALETTES[name] = checkPaletteValid(name, map); + return themingProvider; + } + + // Returns an new object which is a copy of a given palette `name` with variables from + // `map` overwritten + // Example: var neonRedMap = $mdThemingProvider.extendPalette('red', { 50: '#f5fafafa' }); + function extendPalette(name, map) { + return checkPaletteValid(name, angular.extend({}, PALETTES[name] || {}, map) ); + } + + // Make sure that palette has all required hues + function checkPaletteValid(name, map) { + var missingColors = VALID_HUE_VALUES.filter(function(field) { + return !map[field]; + }); + if (missingColors.length) { + throw new Error("Missing colors %1 in palette %2!" + .replace('%1', missingColors.join(', ')) + .replace('%2', name)); + } + + return map; + } + + // Register a theme (which is a collection of color palettes to use with various states + // ie. warn, accent, primary ) + // Optionally inherit from an existing theme + // $mdThemingProvider.theme('custom-theme').primaryPalette('red'); + function registerTheme(name, inheritFrom) { + if (THEMES[name]) return THEMES[name]; + + inheritFrom = inheritFrom || 'default'; + + var parentTheme = typeof inheritFrom === 'string' ? THEMES[inheritFrom] : inheritFrom; + var theme = new Theme(name); + + if (parentTheme) { + angular.forEach(parentTheme.colors, function(color, colorType) { + theme.colors[colorType] = { + name: color.name, + // Make sure a COPY of the hues is given to the child color, + // not the same reference. + hues: angular.extend({}, color.hues) + }; + }); + } + THEMES[name] = theme; + + return theme; + } + + function Theme(name) { + var self = this; + self.name = name; + self.colors = {}; + + self.dark = setDark; + setDark(false); + + function setDark(isDark) { + isDark = arguments.length === 0 ? true : !!isDark; + + // If no change, abort + if (isDark === self.isDark) return; + + self.isDark = isDark; + + self.foregroundPalette = self.isDark ? LIGHT_FOREGROUND : DARK_FOREGROUND; + self.foregroundShadow = self.isDark ? DARK_SHADOW : LIGHT_SHADOW; + + // Light and dark themes have different default hues. + // Go through each existing color type for this theme, and for every + // hue value that is still the default hue value from the previous light/dark setting, + // set it to the default hue value from the new light/dark setting. + var newDefaultHues = self.isDark ? DARK_DEFAULT_HUES : LIGHT_DEFAULT_HUES; + var oldDefaultHues = self.isDark ? LIGHT_DEFAULT_HUES : DARK_DEFAULT_HUES; + angular.forEach(newDefaultHues, function(newDefaults, colorType) { + var color = self.colors[colorType]; + var oldDefaults = oldDefaultHues[colorType]; + if (color) { + for (var hueName in color.hues) { + if (color.hues[hueName] === oldDefaults[hueName]) { + color.hues[hueName] = newDefaults[hueName]; + } + } + } + }); + + return self; + } + + THEME_COLOR_TYPES.forEach(function(colorType) { + var defaultHues = (self.isDark ? DARK_DEFAULT_HUES : LIGHT_DEFAULT_HUES)[colorType]; + self[colorType + 'Palette'] = function setPaletteType(paletteName, hues) { + var color = self.colors[colorType] = { + name: paletteName, + hues: angular.extend({}, defaultHues, hues) + }; + + Object.keys(color.hues).forEach(function(name) { + if (!defaultHues[name]) { + throw new Error("Invalid hue name '%1' in theme %2's %3 color %4. Available hue names: %4" + .replace('%1', name) + .replace('%2', self.name) + .replace('%3', paletteName) + .replace('%4', Object.keys(defaultHues).join(', ')) + ); + } + }); + Object.keys(color.hues).map(function(key) { + return color.hues[key]; + }).forEach(function(hueValue) { + if (VALID_HUE_VALUES.indexOf(hueValue) == -1) { + throw new Error("Invalid hue value '%1' in theme %2's %3 color %4. Available hue values: %5" + .replace('%1', hueValue) + .replace('%2', self.name) + .replace('%3', colorType) + .replace('%4', paletteName) + .replace('%5', VALID_HUE_VALUES.join(', ')) + ); + } + }); + return self; + }; + + self[colorType + 'Color'] = function() { + var args = Array.prototype.slice.call(arguments); + console.warn('$mdThemingProviderTheme.' + colorType + 'Color() has been deprecated. ' + + 'Use $mdThemingProviderTheme.' + colorType + 'Palette() instead.'); + return self[colorType + 'Palette'].apply(self, args); + }; + }); + } + + /** + * @ngdoc service + * @name $mdTheming + * + * @description + * + * Service that makes an element apply theming related classes to itself. + * + * ```js + * app.directive('myFancyDirective', function($mdTheming) { + * return { + * restrict: 'e', + * link: function(scope, el, attrs) { + * $mdTheming(el); + * } + * }; + * }); + * ``` + * @param {el=} element to apply theming to + */ + /* @ngInject */ + function ThemingService($rootScope, $log) { + // Allow us to be invoked via a linking function signature. + var applyTheme = function (scope, el) { + if (el === undefined) { el = scope; scope = undefined; } + if (scope === undefined) { scope = $rootScope; } + applyTheme.inherit(el, el); + }; + + applyTheme.THEMES = angular.extend({}, THEMES); + applyTheme.PALETTES = angular.extend({}, PALETTES); + applyTheme.inherit = inheritTheme; + applyTheme.registered = registered; + applyTheme.defaultTheme = function() { return defaultTheme; }; + applyTheme.generateTheme = function(name) { generateTheme(THEMES[name], name, themeConfig.nonce); }; + applyTheme.setBrowserColor = enableBrowserColor; + + return applyTheme; + + /** + * Determine is specified theme name is a valid, registered theme + */ + function registered(themeName) { + if (themeName === undefined || themeName === '') return true; + return applyTheme.THEMES[themeName] !== undefined; + } + + /** + * Get theme name for the element, then update with Theme CSS class + */ + function inheritTheme (el, parent) { + var ctrl = parent.controller('mdTheme'); + var attrThemeValue = el.attr('md-theme-watch'); + var watchTheme = (alwaysWatchTheme || angular.isDefined(attrThemeValue)) && attrThemeValue != 'false'; + + updateThemeClass(lookupThemeName()); + + if ((alwaysWatchTheme && !registerChangeCallback()) || (!alwaysWatchTheme && watchTheme)) { + el.on('$destroy', $rootScope.$watch(lookupThemeName, updateThemeClass) ); + } + + /** + * Find the theme name from the parent controller or element data + */ + function lookupThemeName() { + // As a few components (dialog) add their controllers later, we should also watch for a controller init. + ctrl = parent.controller('mdTheme') || el.data('$mdThemeController'); + return ctrl && ctrl.$mdTheme || (defaultTheme == 'default' ? '' : defaultTheme); + } + + /** + * Remove old theme class and apply a new one + * NOTE: if not a valid theme name, then the current name is not changed + */ + function updateThemeClass(theme) { + if (!theme) return; + if (!registered(theme)) { + $log.warn('Attempted to use unregistered theme \'' + theme + '\'. ' + + 'Register it with $mdThemingProvider.theme().'); + } + + var oldTheme = el.data('$mdThemeName'); + if (oldTheme) el.removeClass('md-' + oldTheme +'-theme'); + el.addClass('md-' + theme + '-theme'); + el.data('$mdThemeName', theme); + if (ctrl) { + el.data('$mdThemeController', ctrl); + } + } + + /** + * Register change callback with parent mdTheme controller + */ + function registerChangeCallback() { + var parentController = parent.controller('mdTheme'); + if (!parentController) return false; + el.on('$destroy', parentController.registerChanges( function() { + updateThemeClass(lookupThemeName()); + })); + return true; + } + } + + } +} + +function ThemingDirective($mdTheming, $interpolate, $log) { + return { + priority: 100, + link: { + pre: function(scope, el, attrs) { + var registeredCallbacks = []; + var ctrl = { + registerChanges: function (cb, context) { + if (context) { + cb = angular.bind(context, cb); + } + + registeredCallbacks.push(cb); + + return function () { + var index = registeredCallbacks.indexOf(cb); + + if (index > -1) { + registeredCallbacks.splice(index, 1); + } + }; + }, + $setTheme: function (theme) { + if (!$mdTheming.registered(theme)) { + $log.warn('attempted to use unregistered theme \'' + theme + '\''); + } + ctrl.$mdTheme = theme; + + registeredCallbacks.forEach(function (cb) { + cb(); + }); + } + }; + el.data('$mdThemeController', ctrl); + ctrl.$setTheme($interpolate(attrs.mdTheme)(scope)); + attrs.$observe('mdTheme', ctrl.$setTheme); + } + } + }; +} + +/** + * Special directive that will disable ALL runtime Theme style generation and DOM injection + * + * + * + * + * + * ... + * + * + * Note: Using md-themes-css directive requires the developer to load external + * theme stylesheets; e.g. custom themes from Material-Tools: + * + * `angular-material.themes.css` + * + * Another option is to use the ThemingProvider to configure and disable the attribute + * conversions; this would obviate the use of the `md-themes-css` directive + * + */ +function disableThemesDirective() { + themeConfig.disableTheming = true; + + // Return a 1x-only, first-match attribute directive + return { + restrict : 'A', + priority : '900' + }; +} + +function ThemableDirective($mdTheming) { + return $mdTheming; +} + +function parseRules(theme, colorType, rules) { + checkValidPalette(theme, colorType); + + rules = rules.replace(/THEME_NAME/g, theme.name); + var generatedRules = []; + var color = theme.colors[colorType]; + + var themeNameRegex = new RegExp('\\.md-' + theme.name + '-theme', 'g'); + // Matches '{{ primary-color }}', etc + var hueRegex = new RegExp('(\'|")?{{\\s*(' + colorType + ')-(color|contrast)-?(\\d\\.?\\d*)?\\s*}}(\"|\')?','g'); + var simpleVariableRegex = /'?"?\{\{\s*([a-zA-Z]+)-(A?\d+|hue\-[0-3]|shadow|default)-?(\d\.?\d*)?(contrast)?\s*\}\}'?"?/g; + var palette = PALETTES[color.name]; + + // find and replace simple variables where we use a specific hue, not an entire palette + // eg. "{{primary-100}}" + //\(' + THEME_COLOR_TYPES.join('\|') + '\)' + rules = rules.replace(simpleVariableRegex, function(match, colorType, hue, opacity, contrast) { + if (colorType === 'foreground') { + if (hue == 'shadow') { + return theme.foregroundShadow; + } else { + return theme.foregroundPalette[hue] || theme.foregroundPalette['1']; + } + } + + // `default` is also accepted as a hue-value, because the background palettes are + // using it as a name for the default hue. + if (hue.indexOf('hue') === 0 || hue === 'default') { + hue = theme.colors[colorType].hues[hue]; + } + + return rgba( (PALETTES[ theme.colors[colorType].name ][hue] || '')[contrast ? 'contrast' : 'value'], opacity ); + }); + + // For each type, generate rules for each hue (ie. default, md-hue-1, md-hue-2, md-hue-3) + angular.forEach(color.hues, function(hueValue, hueName) { + var newRule = rules + .replace(hueRegex, function(match, _, colorType, hueType, opacity) { + return rgba(palette[hueValue][hueType === 'color' ? 'value' : 'contrast'], opacity); + }); + if (hueName !== 'default') { + newRule = newRule.replace(themeNameRegex, '.md-' + theme.name + '-theme.md-' + hueName); + } + + // Don't apply a selector rule to the default theme, making it easier to override + // styles of the base-component + if (theme.name == 'default') { + var themeRuleRegex = /((?:(?:(?: |>|\.|\w|-|:|\(|\)|\[|\]|"|'|=)+) )?)((?:(?:\w|\.|-)+)?)\.md-default-theme((?: |>|\.|\w|-|:|\(|\)|\[|\]|"|'|=)*)/g; + newRule = newRule.replace(themeRuleRegex, function(match, prefix, target, suffix) { + return match + ', ' + prefix + target + suffix; + }); + } + generatedRules.push(newRule); + }); + + return generatedRules; +} + +var rulesByType = {}; + +// Generate our themes at run time given the state of THEMES and PALETTES +function generateAllThemes($injector, $mdTheming) { + var head = document.head; + var firstChild = head ? head.firstElementChild : null; + var themeCss = !themeConfig.disableTheming && $injector.has('$MD_THEME_CSS') ? $injector.get('$MD_THEME_CSS') : ''; + + // Append our custom registered styles to the theme stylesheet. + themeCss += themeConfig.registeredStyles.join(''); + + if ( !firstChild ) return; + if (themeCss.length === 0) return; // no rules, so no point in running this expensive task + + // Expose contrast colors for palettes to ensure that text is always readable + angular.forEach(PALETTES, sanitizePalette); + + // MD_THEME_CSS is a string generated by the build process that includes all the themable + // components as templates + + // Break the CSS into individual rules + var rules = themeCss + .split(/\}(?!(\}|'|"|;))/) + .filter(function(rule) { return rule && rule.trim().length; }) + .map(function(rule) { return rule.trim() + '}'; }); + + + var ruleMatchRegex = new RegExp('md-(' + THEME_COLOR_TYPES.join('|') + ')', 'g'); + + THEME_COLOR_TYPES.forEach(function(type) { + rulesByType[type] = ''; + }); + + + // Sort the rules based on type, allowing us to do color substitution on a per-type basis + rules.forEach(function(rule) { + var match = rule.match(ruleMatchRegex); + // First: test that if the rule has '.md-accent', it goes into the accent set of rules + for (var i = 0, type; type = THEME_COLOR_TYPES[i]; i++) { + if (rule.indexOf('.md-' + type) > -1) { + return rulesByType[type] += rule; + } + } + + // If no eg 'md-accent' class is found, try to just find 'accent' in the rule and guess from + // there + for (i = 0; type = THEME_COLOR_TYPES[i]; i++) { + if (rule.indexOf(type) > -1) { + return rulesByType[type] += rule; + } + } + + // Default to the primary array + return rulesByType[DEFAULT_COLOR_TYPE] += rule; + }); + + // If themes are being generated on-demand, quit here. The user will later manually + // call generateTheme to do this on a theme-by-theme basis. + if (themeConfig.generateOnDemand) return; + + angular.forEach($mdTheming.THEMES, function(theme) { + if (!GENERATED[theme.name] && !($mdTheming.defaultTheme() !== 'default' && theme.name === 'default')) { + generateTheme(theme, theme.name, themeConfig.nonce); + } + }); + + + // ************************* + // Internal functions + // ************************* + + // The user specifies a 'default' contrast color as either light or dark, + // then explicitly lists which hues are the opposite contrast (eg. A100 has dark, A200 has light) + function sanitizePalette(palette, name) { + var defaultContrast = palette.contrastDefaultColor; + var lightColors = palette.contrastLightColors || []; + var strongLightColors = palette.contrastStrongLightColors || []; + var darkColors = palette.contrastDarkColors || []; + + // These colors are provided as space-separated lists + if (typeof lightColors === 'string') lightColors = lightColors.split(' '); + if (typeof strongLightColors === 'string') strongLightColors = strongLightColors.split(' '); + if (typeof darkColors === 'string') darkColors = darkColors.split(' '); + + // Cleanup after ourselves + delete palette.contrastDefaultColor; + delete palette.contrastLightColors; + delete palette.contrastStrongLightColors; + delete palette.contrastDarkColors; + + // Change { 'A100': '#fffeee' } to { 'A100': { value: '#fffeee', contrast:DARK_CONTRAST_COLOR } + angular.forEach(palette, function(hueValue, hueName) { + if (angular.isObject(hueValue)) return; // Already converted + // Map everything to rgb colors + var rgbValue = colorToRgbaArray(hueValue); + if (!rgbValue) { + throw new Error("Color %1, in palette %2's hue %3, is invalid. Hex or rgb(a) color expected." + .replace('%1', hueValue) + .replace('%2', palette.name) + .replace('%3', hueName)); + } + + palette[hueName] = { + hex: palette[hueName], + value: rgbValue, + contrast: getContrastColor() + }; + function getContrastColor() { + if (defaultContrast === 'light') { + if (darkColors.indexOf(hueName) > -1) { + return DARK_CONTRAST_COLOR; + } else { + return strongLightColors.indexOf(hueName) > -1 ? STRONG_LIGHT_CONTRAST_COLOR + : LIGHT_CONTRAST_COLOR; + } + } else { + if (lightColors.indexOf(hueName) > -1) { + return strongLightColors.indexOf(hueName) > -1 ? STRONG_LIGHT_CONTRAST_COLOR + : LIGHT_CONTRAST_COLOR; + } else { + return DARK_CONTRAST_COLOR; + } + } + } + }); + } +} + +function generateTheme(theme, name, nonce) { + var head = document.head; + var firstChild = head ? head.firstElementChild : null; + + if (!GENERATED[name]) { + // For each theme, use the color palettes specified for + // `primary`, `warn` and `accent` to generate CSS rules. + THEME_COLOR_TYPES.forEach(function(colorType) { + var styleStrings = parseRules(theme, colorType, rulesByType[colorType]); + while (styleStrings.length) { + var styleContent = styleStrings.shift(); + if (styleContent) { + var style = document.createElement('style'); + style.setAttribute('md-theme-style', ''); + if (nonce) { + style.setAttribute('nonce', nonce); + } + style.appendChild(document.createTextNode(styleContent)); + head.insertBefore(style, firstChild); + } + } + }); + + GENERATED[theme.name] = true; + } + +} + + +function checkValidPalette(theme, colorType) { + // If theme attempts to use a palette that doesnt exist, throw error + if (!PALETTES[ (theme.colors[colorType] || {}).name ]) { + throw new Error( + "You supplied an invalid color palette for theme %1's %2 palette. Available palettes: %3" + .replace('%1', theme.name) + .replace('%2', colorType) + .replace('%3', Object.keys(PALETTES).join(', ')) + ); + } +} + +function colorToRgbaArray(clr) { + if (angular.isArray(clr) && clr.length == 3) return clr; + if (/^rgb/.test(clr)) { + return clr.replace(/(^\s*rgba?\(|\)\s*$)/g, '').split(',').map(function(value, i) { + return i == 3 ? parseFloat(value, 10) : parseInt(value, 10); + }); + } + if (clr.charAt(0) == '#') clr = clr.substring(1); + if (!/^([a-fA-F0-9]{3}){1,2}$/g.test(clr)) return; + + var dig = clr.length / 3; + var red = clr.substr(0, dig); + var grn = clr.substr(dig, dig); + var blu = clr.substr(dig * 2); + if (dig === 1) { + red += red; + grn += grn; + blu += blu; + } + return [parseInt(red, 16), parseInt(grn, 16), parseInt(blu, 16)]; +} + +function rgba(rgbArray, opacity) { + if ( !rgbArray ) return "rgb('0,0,0')"; + + if (rgbArray.length == 4) { + rgbArray = angular.copy(rgbArray); + opacity ? rgbArray.pop() : opacity = rgbArray.pop(); + } + return opacity && (typeof opacity == 'number' || (typeof opacity == 'string' && opacity.length)) ? + 'rgba(' + rgbArray.join(',') + ',' + opacity + ')' : + 'rgb(' + rgbArray.join(',') + ')'; +} + + +})(window.angular); + +})(); +(function(){ +"use strict"; + +// Polyfill angular < 1.4 (provide $animateCss) +angular + .module('material.core') + .factory('$$mdAnimate', ["$q", "$timeout", "$mdConstant", "$animateCss", function($q, $timeout, $mdConstant, $animateCss){ + + // Since $$mdAnimate is injected into $mdUtil... use a wrapper function + // to subsequently inject $mdUtil as an argument to the AnimateDomUtils + + return function($mdUtil) { + return AnimateDomUtils( $mdUtil, $q, $timeout, $mdConstant, $animateCss); + }; + }]); + +/** + * Factory function that requires special injections + */ +function AnimateDomUtils($mdUtil, $q, $timeout, $mdConstant, $animateCss) { + var self; + return self = { + /** + * + */ + translate3d : function( target, from, to, options ) { + return $animateCss(target,{ + from:from, + to:to, + addClass:options.transitionInClass, + removeClass:options.transitionOutClass + }) + .start() + .then(function(){ + // Resolve with reverser function... + return reverseTranslate; + }); + + /** + * Specific reversal of the request translate animation above... + */ + function reverseTranslate (newFrom) { + return $animateCss(target, { + to: newFrom || from, + addClass: options.transitionOutClass, + removeClass: options.transitionInClass + }).start(); + + } + }, + + /** + * Listen for transitionEnd event (with optional timeout) + * Announce completion or failure via promise handlers + */ + waitTransitionEnd: function (element, opts) { + var TIMEOUT = 3000; // fallback is 3 secs + + return $q(function(resolve, reject){ + opts = opts || { }; + + // If there is no transition is found, resolve immediately + // + // NOTE: using $mdUtil.nextTick() causes delays/issues + if (noTransitionFound(opts.cachedTransitionStyles)) { + TIMEOUT = 0; + } + + var timer = $timeout(finished, opts.timeout || TIMEOUT); + element.on($mdConstant.CSS.TRANSITIONEND, finished); + + /** + * Upon timeout or transitionEnd, reject or resolve (respectively) this promise. + * NOTE: Make sure this transitionEnd didn't bubble up from a child + */ + function finished(ev) { + if ( ev && ev.target !== element[0]) return; + + if ( ev ) $timeout.cancel(timer); + element.off($mdConstant.CSS.TRANSITIONEND, finished); + + // Never reject since ngAnimate may cause timeouts due missed transitionEnd events + resolve(); + + } + + /** + * Checks whether or not there is a transition. + * + * @param styles The cached styles to use for the calculation. If null, getComputedStyle() + * will be used. + * + * @returns {boolean} True if there is no transition/duration; false otherwise. + */ + function noTransitionFound(styles) { + styles = styles || window.getComputedStyle(element[0]); + + return styles.transitionDuration == '0s' || (!styles.transition && !styles.transitionProperty); + } + + }); + }, + + calculateTransformValues: function (element, originator) { + var origin = originator.element; + var bounds = originator.bounds; + + if (origin || bounds) { + var originBnds = origin ? self.clientRect(origin) || currentBounds() : self.copyRect(bounds); + var dialogRect = self.copyRect(element[0].getBoundingClientRect()); + var dialogCenterPt = self.centerPointFor(dialogRect); + var originCenterPt = self.centerPointFor(originBnds); + + return { + centerX: originCenterPt.x - dialogCenterPt.x, + centerY: originCenterPt.y - dialogCenterPt.y, + scaleX: Math.round(100 * Math.min(0.5, originBnds.width / dialogRect.width)) / 100, + scaleY: Math.round(100 * Math.min(0.5, originBnds.height / dialogRect.height)) / 100 + }; + } + return {centerX: 0, centerY: 0, scaleX: 0.5, scaleY: 0.5}; + + /** + * This is a fallback if the origin information is no longer valid, then the + * origin bounds simply becomes the current bounds for the dialogContainer's parent + */ + function currentBounds() { + var cntr = element ? element.parent() : null; + var parent = cntr ? cntr.parent() : null; + + return parent ? self.clientRect(parent) : null; + } + }, + + /** + * Calculate the zoom transform from dialog to origin. + * + * We use this to set the dialog position immediately; + * then the md-transition-in actually translates back to + * `translate3d(0,0,0) scale(1.0)`... + * + * NOTE: all values are rounded to the nearest integer + */ + calculateZoomToOrigin: function (element, originator) { + var zoomTemplate = "translate3d( {centerX}px, {centerY}px, 0 ) scale( {scaleX}, {scaleY} )"; + var buildZoom = angular.bind(null, $mdUtil.supplant, zoomTemplate); + + return buildZoom(self.calculateTransformValues(element, originator)); + }, + + /** + * Calculate the slide transform from panel to origin. + * NOTE: all values are rounded to the nearest integer + */ + calculateSlideToOrigin: function (element, originator) { + var slideTemplate = "translate3d( {centerX}px, {centerY}px, 0 )"; + var buildSlide = angular.bind(null, $mdUtil.supplant, slideTemplate); + + return buildSlide(self.calculateTransformValues(element, originator)); + }, + + /** + * Enhance raw values to represent valid css stylings... + */ + toCss : function( raw ) { + var css = { }; + var lookups = 'left top right bottom width height x y min-width min-height max-width max-height'; + + angular.forEach(raw, function(value,key) { + if ( angular.isUndefined(value) ) return; + + if ( lookups.indexOf(key) >= 0 ) { + css[key] = value + 'px'; + } else { + switch (key) { + case 'transition': + convertToVendor(key, $mdConstant.CSS.TRANSITION, value); + break; + case 'transform': + convertToVendor(key, $mdConstant.CSS.TRANSFORM, value); + break; + case 'transformOrigin': + convertToVendor(key, $mdConstant.CSS.TRANSFORM_ORIGIN, value); + break; + case 'font-size': + css['font-size'] = value; // font sizes aren't always in px + break; + } + } + }); + + return css; + + function convertToVendor(key, vendor, value) { + angular.forEach(vendor.split(' '), function (key) { + css[key] = value; + }); + } + }, + + /** + * Convert the translate CSS value to key/value pair(s). + */ + toTransformCss: function (transform, addTransition, transition) { + var css = {}; + angular.forEach($mdConstant.CSS.TRANSFORM.split(' '), function (key) { + css[key] = transform; + }); + + if (addTransition) { + transition = transition || "all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1) !important"; + css.transition = transition; + } + + return css; + }, + + /** + * Clone the Rect and calculate the height/width if needed + */ + copyRect: function (source, destination) { + if (!source) return null; + + destination = destination || {}; + + angular.forEach('left top right bottom width height'.split(' '), function (key) { + destination[key] = Math.round(source[key]); + }); + + destination.width = destination.width || (destination.right - destination.left); + destination.height = destination.height || (destination.bottom - destination.top); + + return destination; + }, + + /** + * Calculate ClientRect of element; return null if hidden or zero size + */ + clientRect: function (element) { + var bounds = angular.element(element)[0].getBoundingClientRect(); + var isPositiveSizeClientRect = function (rect) { + return rect && (rect.width > 0) && (rect.height > 0); + }; + + // If the event origin element has zero size, it has probably been hidden. + return isPositiveSizeClientRect(bounds) ? self.copyRect(bounds) : null; + }, + + /** + * Calculate 'rounded' center point of Rect + */ + centerPointFor: function (targetRect) { + return targetRect ? { + x: Math.round(targetRect.left + (targetRect.width / 2)), + y: Math.round(targetRect.top + (targetRect.height / 2)) + } : { x : 0, y : 0 }; + } + + }; +} + + +})(); +(function(){ +"use strict"; + +"use strict"; + +if (angular.version.minor >= 4) { + angular.module('material.core.animate', []); +} else { +(function() { + + var forEach = angular.forEach; + + var WEBKIT = angular.isDefined(document.documentElement.style.WebkitAppearance); + var TRANSITION_PROP = WEBKIT ? 'WebkitTransition' : 'transition'; + var ANIMATION_PROP = WEBKIT ? 'WebkitAnimation' : 'animation'; + var PREFIX = WEBKIT ? '-webkit-' : ''; + + var TRANSITION_EVENTS = (WEBKIT ? 'webkitTransitionEnd ' : '') + 'transitionend'; + var ANIMATION_EVENTS = (WEBKIT ? 'webkitAnimationEnd ' : '') + 'animationend'; + + var $$ForceReflowFactory = ['$document', function($document) { + return function() { + return $document[0].body.clientWidth + 1; + } + }]; + + var $$rAFMutexFactory = ['$$rAF', function($$rAF) { + return function() { + var passed = false; + $$rAF(function() { + passed = true; + }); + return function(fn) { + passed ? fn() : $$rAF(fn); + }; + }; + }]; + + var $$AnimateRunnerFactory = ['$q', '$$rAFMutex', function($q, $$rAFMutex) { + var INITIAL_STATE = 0; + var DONE_PENDING_STATE = 1; + var DONE_COMPLETE_STATE = 2; + + function AnimateRunner(host) { + this.setHost(host); + + this._doneCallbacks = []; + this._runInAnimationFrame = $$rAFMutex(); + 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: angular.noop, + + getPromise: function() { + if (!this.promise) { + var self = this; + this.promise = $q(function(resolve, reject) { + self.done(function(status) { + status === false ? reject() : 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._runInAnimationFrame(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; + } + } + }; + + // Polyfill AnimateRunner.all which is used by input animations + 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); + } + } + }; + + return AnimateRunner; + }]; + + angular + .module('material.core.animate', []) + .factory('$$forceReflow', $$ForceReflowFactory) + .factory('$$AnimateRunner', $$AnimateRunnerFactory) + .factory('$$rAFMutex', $$rAFMutexFactory) + .factory('$animateCss', ['$window', '$$rAF', '$$AnimateRunner', '$$forceReflow', '$$jqLite', '$timeout', '$animate', + function($window, $$rAF, $$AnimateRunner, $$forceReflow, $$jqLite, $timeout, $animate) { + + function init(element, options) { + + var temporaryStyles = []; + var node = getDomNode(element); + var areAnimationsAllowed = node && $animate.enabled(); + + var hasCompleteStyles = false; + var hasCompleteClasses = false; + + if (areAnimationsAllowed) { + if (options.transitionStyle) { + temporaryStyles.push([PREFIX + 'transition', options.transitionStyle]); + } + + if (options.keyframeStyle) { + temporaryStyles.push([PREFIX + 'animation', options.keyframeStyle]); + } + + if (options.delay) { + temporaryStyles.push([PREFIX + 'transition-delay', options.delay + 's']); + } + + if (options.duration) { + temporaryStyles.push([PREFIX + 'transition-duration', options.duration + 's']); + } + + hasCompleteStyles = options.keyframeStyle || + (options.to && (options.duration > 0 || options.transitionStyle)); + hasCompleteClasses = !!options.addClass || !!options.removeClass; + + blockTransition(element, true); + } + + var hasCompleteAnimation = areAnimationsAllowed && (hasCompleteStyles || hasCompleteClasses); + + applyAnimationFromStyles(element, options); + + var animationClosed = false; + var events, eventFn; + + return { + close: $window.close, + start: function() { + var runner = new $$AnimateRunner(); + waitUntilQuiet(function() { + blockTransition(element, false); + if (!hasCompleteAnimation) { + return close(); + } + + forEach(temporaryStyles, function(entry) { + var key = entry[0]; + var value = entry[1]; + node.style[camelCase(key)] = value; + }); + + applyClasses(element, options); + + var timings = computeTimings(element); + if (timings.duration === 0) { + return close(); + } + + var moreStyles = []; + + if (options.easing) { + if (timings.transitionDuration) { + moreStyles.push([PREFIX + 'transition-timing-function', options.easing]); + } + if (timings.animationDuration) { + moreStyles.push([PREFIX + 'animation-timing-function', options.easing]); + } + } + + if (options.delay && timings.animationDelay) { + moreStyles.push([PREFIX + 'animation-delay', options.delay + 's']); + } + + if (options.duration && timings.animationDuration) { + moreStyles.push([PREFIX + 'animation-duration', options.duration + 's']); + } + + forEach(moreStyles, function(entry) { + var key = entry[0]; + var value = entry[1]; + node.style[camelCase(key)] = value; + temporaryStyles.push(entry); + }); + + var maxDelay = timings.delay; + var maxDelayTime = maxDelay * 1000; + var maxDuration = timings.duration; + var maxDurationTime = maxDuration * 1000; + var startTime = Date.now(); + + events = []; + if (timings.transitionDuration) { + events.push(TRANSITION_EVENTS); + } + if (timings.animationDuration) { + events.push(ANIMATION_EVENTS); + } + events = events.join(' '); + eventFn = function(event) { + event.stopPropagation(); + var ev = event.originalEvent || event; + var timeStamp = ev.timeStamp || Date.now(); + var elapsedTime = parseFloat(ev.elapsedTime.toFixed(3)); + if (Math.max(timeStamp - startTime, 0) >= maxDelayTime && elapsedTime >= maxDuration) { + close(); + } + }; + element.on(events, eventFn); + + applyAnimationToStyles(element, options); + + $timeout(close, maxDelayTime + maxDurationTime * 1.5, false); + }); + + return runner; + + function close() { + if (animationClosed) return; + animationClosed = true; + + if (events && eventFn) { + element.off(events, eventFn); + } + applyClasses(element, options); + applyAnimationStyles(element, options); + forEach(temporaryStyles, function(entry) { + node.style[camelCase(entry[0])] = ''; + }); + runner.complete(true); + return runner; + } + } + } + } + + function applyClasses(element, options) { + if (options.addClass) { + $$jqLite.addClass(element, options.addClass); + options.addClass = null; + } + if (options.removeClass) { + $$jqLite.removeClass(element, options.removeClass); + options.removeClass = null; + } + } + + function computeTimings(element) { + var node = getDomNode(element); + var cs = $window.getComputedStyle(node) + var tdr = parseMaxTime(cs[prop('transitionDuration')]); + var adr = parseMaxTime(cs[prop('animationDuration')]); + var tdy = parseMaxTime(cs[prop('transitionDelay')]); + var ady = parseMaxTime(cs[prop('animationDelay')]); + + adr *= (parseInt(cs[prop('animationIterationCount')], 10) || 1); + var duration = Math.max(adr, tdr); + var delay = Math.max(ady, tdy); + + return { + duration: duration, + delay: delay, + animationDuration: adr, + transitionDuration: tdr, + animationDelay: ady, + transitionDelay: tdy + }; + + function prop(key) { + return WEBKIT ? 'Webkit' + key.charAt(0).toUpperCase() + key.substr(1) + : key; + } + } + + function parseMaxTime(str) { + var maxValue = 0; + var values = (str || "").split(/\s*,\s*/); + forEach(values, function(value) { + // it's always safe to consider only second values and omit `ms` values since + // getComputedStyle will always handle the conversion for us + if (value.charAt(value.length - 1) == 's') { + value = value.substring(0, value.length - 1); + } + value = parseFloat(value) || 0; + maxValue = maxValue ? Math.max(value, maxValue) : value; + }); + return maxValue; + } + + var cancelLastRAFRequest; + var rafWaitQueue = []; + function waitUntilQuiet(callback) { + if (cancelLastRAFRequest) { + cancelLastRAFRequest(); //cancels the request + } + rafWaitQueue.push(callback); + cancelLastRAFRequest = $$rAF(function() { + cancelLastRAFRequest = null; + + // DO NOT REMOVE THIS LINE OR REFACTOR OUT THE `pageWidth` variable. + // PLEASE EXAMINE THE `$$forceReflow` service to understand why. + var pageWidth = $$forceReflow(); + + // we use a for loop to ensure that if the queue is changed + // during this looping then it will consider new requests + for (var i = 0; i < rafWaitQueue.length; i++) { + rafWaitQueue[i](pageWidth); + } + rafWaitQueue.length = 0; + }); + } + + function applyAnimationStyles(element, options) { + applyAnimationFromStyles(element, options); + applyAnimationToStyles(element, options); + } + + function applyAnimationFromStyles(element, options) { + if (options.from) { + element.css(options.from); + options.from = null; + } + } + + function applyAnimationToStyles(element, options) { + if (options.to) { + element.css(options.to); + options.to = null; + } + } + + function getDomNode(element) { + for (var i = 0; i < element.length; i++) { + if (element[i].nodeType === 1) return element[i]; + } + } + + function blockTransition(element, bool) { + var node = getDomNode(element); + var key = camelCase(PREFIX + 'transition-delay'); + node.style[key] = bool ? '-9999s' : ''; + } + + return init; + }]); + + /** + * Older browsers [FF31] expect camelCase + * property keys. + * e.g. + * animation-duration --> animationDuration + */ + function camelCase(str) { + return str.replace(/-[a-z]/g, function(str) { + return str.charAt(1).toUpperCase(); + }); + } + +})(); + +} + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.components.autocomplete + */ +/* + * @see js folder for autocomplete implementation + */ +angular.module('material.components.autocomplete', [ + 'material.core', + 'material.components.icon', + 'material.components.virtualRepeat' +]); + +})(); +(function(){ +"use strict"; + +/* + * @ngdoc module + * @name material.components.backdrop + * @description Backdrop + */ + +/** + * @ngdoc directive + * @name mdBackdrop + * @module material.components.backdrop + * + * @restrict E + * + * @description + * `` is a backdrop element used by other components, such as dialog and bottom sheet. + * Apply class `opaque` to make the backdrop use the theme backdrop color. + * + */ + +angular + .module('material.components.backdrop', ['material.core']) + .directive('mdBackdrop', ["$mdTheming", "$mdUtil", "$animate", "$rootElement", "$window", "$log", "$$rAF", "$document", function BackdropDirective($mdTheming, $mdUtil, $animate, $rootElement, $window, $log, $$rAF, $document) { + var ERROR_CSS_POSITION = ' may not work properly in a scrolled, static-positioned parent container.'; + + return { + restrict: 'E', + link: postLink + }; + + function postLink(scope, element, attrs) { + // backdrop may be outside the $rootElement, tell ngAnimate to animate regardless + if ($animate.pin) $animate.pin(element, $rootElement); + + var bodyStyles; + + $$rAF(function() { + // If body scrolling has been disabled using mdUtil.disableBodyScroll(), + // adjust the 'backdrop' height to account for the fixed 'body' top offset. + // Note that this can be pretty expensive and is better done inside the $$rAF. + bodyStyles = $window.getComputedStyle($document[0].body); + + if (bodyStyles.position === 'fixed') { + var resizeHandler = $mdUtil.debounce(function(){ + bodyStyles = $window.getComputedStyle($document[0].body); + resize(); + }, 60, null, false); + + resize(); + angular.element($window).on('resize', resizeHandler); + + scope.$on('$destroy', function() { + angular.element($window).off('resize', resizeHandler); + }); + } + + // Often $animate.enter() is used to append the backDrop element + // so let's wait until $animate is done... + var parent = element.parent(); + + if (parent.length) { + if (parent[0].nodeName === 'BODY') { + element.css('position', 'fixed'); + } + + var styles = $window.getComputedStyle(parent[0]); + + if (styles.position === 'static') { + // backdrop uses position:absolute and will not work properly with parent position:static (default) + $log.warn(ERROR_CSS_POSITION); + } + + // Only inherit the parent if the backdrop has a parent. + $mdTheming.inherit(element, parent); + } + }); + + function resize() { + var viewportHeight = parseInt(bodyStyles.height, 10) + Math.abs(parseInt(bodyStyles.top, 10)); + element.css('height', viewportHeight + 'px'); + } + } + + }]); + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.components.bottomSheet + * @description + * BottomSheet + */ +MdBottomSheetDirective.$inject = ["$mdBottomSheet"]; +MdBottomSheetProvider.$inject = ["$$interimElementProvider"]; +angular + .module('material.components.bottomSheet', [ + 'material.core', + 'material.components.backdrop' + ]) + .directive('mdBottomSheet', MdBottomSheetDirective) + .provider('$mdBottomSheet', MdBottomSheetProvider); + +/* @ngInject */ +function MdBottomSheetDirective($mdBottomSheet) { + return { + restrict: 'E', + link : function postLink(scope, element) { + element.addClass('_md'); // private md component indicator for styling + + // When navigation force destroys an interimElement, then + // listen and $destroy() that interim instance... + scope.$on('$destroy', function() { + $mdBottomSheet.destroy(); + }); + } + }; +} + + +/** + * @ngdoc service + * @name $mdBottomSheet + * @module material.components.bottomSheet + * + * @description + * `$mdBottomSheet` opens a bottom sheet over the app and provides a simple promise API. + * + * ## Restrictions + * + * - The bottom sheet's template must have an outer `` element. + * - Add the `md-grid` class to the bottom sheet for a grid layout. + * - Add the `md-list` class to the bottom sheet for a list layout. + * + * @usage + * + *
      + * + * Open a Bottom Sheet! + * + *
      + *
      + * + * var app = angular.module('app', ['ngMaterial']); + * app.controller('MyController', function($scope, $mdBottomSheet) { + * $scope.openBottomSheet = function() { + * $mdBottomSheet.show({ + * template: 'Hello!' + * }); + * }; + * }); + * + */ + + /** + * @ngdoc method + * @name $mdBottomSheet#show + * + * @description + * Show a bottom sheet with the specified options. + * + * @param {object} options An options object, with the following properties: + * + * - `templateUrl` - `{string=}`: The url of an html template file that will + * be used as the content of the bottom sheet. Restrictions: the template must + * have an outer `md-bottom-sheet` element. + * - `template` - `{string=}`: Same as templateUrl, except this is an actual + * template string. + * - `scope` - `{object=}`: the scope to link the template / controller to. If none is specified, it will create a new child scope. + * This scope will be destroyed when the bottom sheet is removed unless `preserveScope` is set to true. + * - `preserveScope` - `{boolean=}`: whether to preserve the scope when the element is removed. Default is false + * - `controller` - `{string=}`: The controller to associate with this bottom sheet. + * - `locals` - `{string=}`: An object containing key/value pairs. The keys will + * be used as names of values to inject into the controller. For example, + * `locals: {three: 3}` would inject `three` into the controller with the value + * of 3. + * - `clickOutsideToClose` - `{boolean=}`: Whether the user can click outside the bottom sheet to + * close it. Default true. + * - `disableBackdrop` - `{boolean=}`: When set to true, the bottomsheet will not show a backdrop. + * - `escapeToClose` - `{boolean=}`: Whether the user can press escape to close the bottom sheet. + * Default true. + * - `resolve` - `{object=}`: Similar to locals, except it takes promises as values + * and the bottom sheet will not open until the promises resolve. + * - `controllerAs` - `{string=}`: An alias to assign the controller to on the scope. + * - `parent` - `{element=}`: The element to append the bottom sheet to. The `parent` may be a `function`, `string`, + * `object`, or null. Defaults to appending to the body of the root element (or the root element) of the application. + * e.g. angular.element(document.getElementById('content')) or "#content" + * - `disableParentScroll` - `{boolean=}`: Whether to disable scrolling while the bottom sheet is open. + * Default true. + * + * @returns {promise} A promise that can be resolved with `$mdBottomSheet.hide()` or + * rejected with `$mdBottomSheet.cancel()`. + */ + +/** + * @ngdoc method + * @name $mdBottomSheet#hide + * + * @description + * Hide the existing bottom sheet and resolve the promise returned from + * `$mdBottomSheet.show()`. This call will close the most recently opened/current bottomsheet (if any). + * + * @param {*=} response An argument for the resolved promise. + * + */ + +/** + * @ngdoc method + * @name $mdBottomSheet#cancel + * + * @description + * Hide the existing bottom sheet and reject the promise returned from + * `$mdBottomSheet.show()`. + * + * @param {*=} response An argument for the rejected promise. + * + */ + +function MdBottomSheetProvider($$interimElementProvider) { + // how fast we need to flick down to close the sheet, pixels/ms + bottomSheetDefaults.$inject = ["$animate", "$mdConstant", "$mdUtil", "$mdTheming", "$mdBottomSheet", "$rootElement", "$mdGesture", "$log"]; + var CLOSING_VELOCITY = 0.5; + var PADDING = 80; // same as css + + return $$interimElementProvider('$mdBottomSheet') + .setDefaults({ + methods: ['disableParentScroll', 'escapeToClose', 'clickOutsideToClose'], + options: bottomSheetDefaults + }); + + /* @ngInject */ + function bottomSheetDefaults($animate, $mdConstant, $mdUtil, $mdTheming, $mdBottomSheet, $rootElement, + $mdGesture, $log) { + var backdrop; + + return { + themable: true, + onShow: onShow, + onRemove: onRemove, + disableBackdrop: false, + escapeToClose: true, + clickOutsideToClose: true, + disableParentScroll: true + }; + + + function onShow(scope, element, options, controller) { + + element = $mdUtil.extractElementByName(element, 'md-bottom-sheet'); + + // prevent tab focus or click focus on the bottom-sheet container + element.attr('tabindex',"-1"); + + // Once the md-bottom-sheet has `ng-cloak` applied on his template the opening animation will not work properly. + // This is a very common problem, so we have to notify the developer about this. + if (element.hasClass('ng-cloak')) { + var message = '$mdBottomSheet: using `` will affect the bottom-sheet opening animations.'; + $log.warn( message, element[0] ); + } + + if (!options.disableBackdrop) { + // Add a backdrop that will close on click + backdrop = $mdUtil.createBackdrop(scope, "md-bottom-sheet-backdrop md-opaque"); + + // Prevent mouse focus on backdrop; ONLY programatic focus allowed. + // This allows clicks on backdrop to propogate to the $rootElement and + // ESC key events to be detected properly. + + backdrop[0].tabIndex = -1; + + if (options.clickOutsideToClose) { + backdrop.on('click', function() { + $mdUtil.nextTick($mdBottomSheet.cancel,true); + }); + } + + $mdTheming.inherit(backdrop, options.parent); + + $animate.enter(backdrop, options.parent, null); + } + + var bottomSheet = new BottomSheet(element, options.parent); + options.bottomSheet = bottomSheet; + + $mdTheming.inherit(bottomSheet.element, options.parent); + + if (options.disableParentScroll) { + options.restoreScroll = $mdUtil.disableScrollAround(bottomSheet.element, options.parent); + } + + return $animate.enter(bottomSheet.element, options.parent, backdrop) + .then(function() { + var focusable = $mdUtil.findFocusTarget(element) || angular.element( + element[0].querySelector('button') || + element[0].querySelector('a') || + element[0].querySelector($mdUtil.prefixer('ng-click', true)) + ) || backdrop; + + if (options.escapeToClose) { + options.rootElementKeyupCallback = function(e) { + if (e.keyCode === $mdConstant.KEY_CODE.ESCAPE) { + $mdUtil.nextTick($mdBottomSheet.cancel,true); + } + }; + + $rootElement.on('keyup', options.rootElementKeyupCallback); + focusable && focusable.focus(); + } + }); + + } + + function onRemove(scope, element, options) { + + var bottomSheet = options.bottomSheet; + + if (!options.disableBackdrop) $animate.leave(backdrop); + return $animate.leave(bottomSheet.element).then(function() { + if (options.disableParentScroll) { + options.restoreScroll(); + delete options.restoreScroll; + } + + bottomSheet.cleanup(); + }); + } + + /** + * BottomSheet class to apply bottom-sheet behavior to an element + */ + function BottomSheet(element, parent) { + var deregister = $mdGesture.register(parent, 'drag', { horizontal: false }); + parent.on('$md.dragstart', onDragStart) + .on('$md.drag', onDrag) + .on('$md.dragend', onDragEnd); + + return { + element: element, + cleanup: function cleanup() { + deregister(); + parent.off('$md.dragstart', onDragStart); + parent.off('$md.drag', onDrag); + parent.off('$md.dragend', onDragEnd); + } + }; + + function onDragStart(ev) { + // Disable transitions on transform so that it feels fast + element.css($mdConstant.CSS.TRANSITION_DURATION, '0ms'); + } + + function onDrag(ev) { + var transform = ev.pointer.distanceY; + if (transform < 5) { + // Slow down drag when trying to drag up, and stop after PADDING + transform = Math.max(-PADDING, transform / 2); + } + element.css($mdConstant.CSS.TRANSFORM, 'translate3d(0,' + (PADDING + transform) + 'px,0)'); + } + + function onDragEnd(ev) { + if (ev.pointer.distanceY > 0 && + (ev.pointer.distanceY > 20 || Math.abs(ev.pointer.velocityY) > CLOSING_VELOCITY)) { + var distanceRemaining = element.prop('offsetHeight') - ev.pointer.distanceY; + var transitionDuration = Math.min(distanceRemaining / ev.pointer.velocityY * 0.75, 500); + element.css($mdConstant.CSS.TRANSITION_DURATION, transitionDuration + 'ms'); + $mdUtil.nextTick($mdBottomSheet.cancel,true); + } else { + element.css($mdConstant.CSS.TRANSITION_DURATION, ''); + element.css($mdConstant.CSS.TRANSFORM, ''); + } + } + } + + } + +} + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.components.button + * @description + * + * Button + */ +MdButtonDirective.$inject = ["$mdButtonInkRipple", "$mdTheming", "$mdAria", "$timeout"]; +MdAnchorDirective.$inject = ["$mdTheming"]; +angular + .module('material.components.button', [ 'material.core' ]) + .directive('mdButton', MdButtonDirective) + .directive('a', MdAnchorDirective); + + +/** + * @private + * @restrict E + * + * @description + * `a` is an anchor directive used to inherit theme colors for md-primary, md-accent, etc. + * + * @usage + * + * + * + * + * + * + */ +function MdAnchorDirective($mdTheming) { + return { + restrict : 'E', + link : function postLink(scope, element) { + // Make sure to inherit theme so stand-alone anchors + // support theme colors for md-primary, md-accent, etc. + $mdTheming(element); + } + }; +} + + +/** + * @ngdoc directive + * @name mdButton + * @module material.components.button + * + * @restrict E + * + * @description + * `` is a button directive with optional ink ripples (default enabled). + * + * If you supply a `href` or `ng-href` attribute, it will become an `` element. Otherwise, it + * will become a `'; + } + } + + function postLink(scope, element, attr) { + $mdTheming(element); + $mdButtonInkRipple.attach(scope, element); + + // Use async expect to support possible bindings in the button label + $mdAria.expectWithoutText(element, 'aria-label'); + + // For anchor elements, we have to set tabindex manually when the + // element is disabled + if (isAnchor(attr) && angular.isDefined(attr.ngDisabled) ) { + scope.$watch(attr.ngDisabled, function(isDisabled) { + element.attr('tabindex', isDisabled ? -1 : 0); + }); + } + + // disabling click event when disabled is true + element.on('click', function(e){ + if (attr.disabled === true) { + e.preventDefault(); + e.stopImmediatePropagation(); + } + }); + + if (!element.hasClass('md-no-focus')) { + // restrict focus styles to the keyboard + scope.mouseActive = false; + element.on('mousedown', function() { + scope.mouseActive = true; + $timeout(function(){ + scope.mouseActive = false; + }, 100); + }) + .on('focus', function() { + if (scope.mouseActive === false) { + element.addClass('md-focused'); + } + }) + .on('blur', function(ev) { + element.removeClass('md-focused'); + }); + } + + } + +} + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.components.card + * + * @description + * Card components. + */ +mdCardDirective.$inject = ["$mdTheming"]; +angular.module('material.components.card', [ + 'material.core' + ]) + .directive('mdCard', mdCardDirective); + + +/** + * @ngdoc directive + * @name mdCard + * @module material.components.card + * + * @restrict E + * + * @description + * The `` directive is a container element used within `` containers. + * + * An image included as a direct descendant will fill the card's width. If you want to avoid this, + * you can add the `md-image-no-fill` class to the parent element. The `` + * container will wrap text content and provide padding. An `` element can be + * optionally included to put content flush against the bottom edge of the card. + * + * Action buttons can be included in an `` element, similar to ``. + * You can then position buttons using layout attributes. + * + * Card is built with: + * * `` - Header for the card, holds avatar, text and squared image + * - `` - Card avatar + * - `md-user-avatar` - Class for user image + * - `` + * - `` - Contains elements for the card description + * - `md-title` - Class for the card title + * - `md-subhead` - Class for the card sub header + * * `` - Image for the card + * * `` - Card content title + * - `` + * - `md-headline` - Class for the card content title + * - `md-subhead` - Class for the card content sub header + * - `` - Squared image within the title + * - `md-media-sm` - Class for small image + * - `md-media-md` - Class for medium image + * - `md-media-lg` - Class for large image + * * `` - Card content + * - `md-media-xl` - Class for extra large image + * * `` - Card actions + * - `` - Icon actions + * + * Cards have constant width and variable heights; where the maximum height is limited to what can + * fit within a single view on a platform, but it can temporarily expand as needed. + * + * @usage + * ### Card with optional footer + * + * + * image caption + * + *

      Card headline

      + *

      Card content

      + *
      + * + * Card footer + * + *
      + *
      + * + * ### Card with actions + * + * + * image caption + * + *

      Card headline

      + *

      Card content

      + *
      + * + * Action 1 + * Action 2 + * + *
      + *
      + * + * ### Card with header, image, title actions and content + * + * + * + * + * + * + * + * Title + * Sub header + * + * + * image caption + * + * + * Card headline + * Card subheader + * + * + * + * Action 1 + * Action 2 + * + * + * + * + * + * + * + *

      + * Card content + *

      + *
      + *
      + *
      + */ +function mdCardDirective($mdTheming) { + return { + restrict: 'E', + link: function ($scope, $element, attr) { + $element.addClass('_md'); // private md component indicator for styling + $mdTheming($element); + } + }; +} + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.components.chips + */ +/* + * @see js folder for chips implementation + */ +angular.module('material.components.chips', [ + 'material.core', + 'material.components.autocomplete' +]); + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.components.checkbox + * @description Checkbox module! + */ +MdCheckboxDirective.$inject = ["inputDirective", "$mdAria", "$mdConstant", "$mdTheming", "$mdUtil", "$timeout"]; +angular + .module('material.components.checkbox', ['material.core']) + .directive('mdCheckbox', MdCheckboxDirective); + +/** + * @ngdoc directive + * @name mdCheckbox + * @module material.components.checkbox + * @restrict E + * + * @description + * The checkbox directive is used like the normal [angular checkbox](https://docs.angularjs.org/api/ng/input/input%5Bcheckbox%5D). + * + * As per the [material design spec](http://www.google.com/design/spec/style/color.html#color-color-schemes) + * the checkbox is in the accent color by default. The primary color palette may be used with + * the `md-primary` class. + * + * @param {string} ng-model Assignable angular expression to data-bind to. + * @param {string=} name Property name of the form under which the control is published. + * @param {expression=} ng-true-value The value to which the expression should be set when selected. + * @param {expression=} ng-false-value The value to which the expression should be set when not selected. + * @param {string=} ng-change Angular expression to be executed when input changes due to user interaction with the input element. + * @param {boolean=} md-no-ink Use of attribute indicates use of ripple ink effects + * @param {string=} aria-label Adds label to checkbox for accessibility. + * Defaults to checkbox's text. If no default text is found, a warning will be logged. + * @param {expression=} md-indeterminate This determines when the checkbox should be rendered as 'indeterminate'. + * If a truthy expression or no value is passed in the checkbox renders in the md-indeterminate state. + * If falsy expression is passed in it just looks like a normal unchecked checkbox. + * The indeterminate, checked, and unchecked states are mutually exclusive. A box cannot be in any two states at the same time. + * Adding the 'md-indeterminate' attribute overrides any checked/unchecked rendering logic. + * When using the 'md-indeterminate' attribute use 'ng-checked' to define rendering logic instead of using 'ng-model'. + * @param {expression=} ng-checked If this expression evaluates as truthy, the 'md-checked' css class is added to the checkbox and it + * will appear checked. + * + * @usage + * + * + * Finished ? + * + * + * + * No Ink Effects + * + * + * + * Disabled + * + * + * + * + */ +function MdCheckboxDirective(inputDirective, $mdAria, $mdConstant, $mdTheming, $mdUtil, $timeout) { + inputDirective = inputDirective[0]; + + return { + restrict: 'E', + transclude: true, + require: '?ngModel', + priority: 210, // Run before ngAria + template: + '
      ' + + '
      ' + + '
      ' + + '
      ', + compile: compile + }; + + // ********************************************************** + // Private Methods + // ********************************************************** + + function compile (tElement, tAttrs) { + tAttrs.$set('tabindex', tAttrs.tabindex || '0'); + tAttrs.$set('type', 'checkbox'); + tAttrs.$set('role', tAttrs.type); + + return { + pre: function(scope, element) { + // Attach a click handler during preLink, in order to immediately stop propagation + // (especially for ng-click) when the checkbox is disabled. + element.on('click', function(e) { + if (this.hasAttribute('disabled')) { + e.stopImmediatePropagation(); + } + }); + }, + post: postLink + }; + + function postLink(scope, element, attr, ngModelCtrl) { + var isIndeterminate; + ngModelCtrl = ngModelCtrl || $mdUtil.fakeNgModel(); + $mdTheming(element); + + // Redirect focus events to the root element, because IE11 is always focusing the container element instead + // of the md-checkbox element. This causes issues when using ngModelOptions: `updateOnBlur` + element.children().on('focus', function() { + element.focus(); + }); + + if ($mdUtil.parseAttributeBoolean(attr.mdIndeterminate)) { + setIndeterminateState(); + scope.$watch(attr.mdIndeterminate, setIndeterminateState); + } + + if (attr.ngChecked) { + scope.$watch(scope.$eval.bind(scope, attr.ngChecked), function(value) { + ngModelCtrl.$setViewValue(value); + ngModelCtrl.$render(); + }); + } + + $$watchExpr('ngDisabled', 'tabindex', { + true: '-1', + false: attr.tabindex + }); + + $mdAria.expectWithText(element, 'aria-label'); + + // Reuse the original input[type=checkbox] directive from Angular core. + // This is a bit hacky as we need our own event listener and own render + // function. + inputDirective.link.pre(scope, { + on: angular.noop, + 0: {} + }, attr, [ngModelCtrl]); + + scope.mouseActive = false; + element.on('click', listener) + .on('keypress', keypressHandler) + .on('mousedown', function() { + scope.mouseActive = true; + $timeout(function() { + scope.mouseActive = false; + }, 100); + }) + .on('focus', function() { + if (scope.mouseActive === false) { + element.addClass('md-focused'); + } + }) + .on('blur', function() { + element.removeClass('md-focused'); + }); + + ngModelCtrl.$render = render; + + function $$watchExpr(expr, htmlAttr, valueOpts) { + if (attr[expr]) { + scope.$watch(attr[expr], function(val) { + if (valueOpts[val]) { + element.attr(htmlAttr, valueOpts[val]); + } + }); + } + } + + function keypressHandler(ev) { + var keyCode = ev.which || ev.keyCode; + if (keyCode === $mdConstant.KEY_CODE.SPACE || keyCode === $mdConstant.KEY_CODE.ENTER) { + ev.preventDefault(); + element.addClass('md-focused'); + listener(ev); + } + } + + function listener(ev) { + // skipToggle boolean is used by the switch directive to prevent the click event + // when releasing the drag. There will be always a click if releasing the drag over the checkbox + if (element[0].hasAttribute('disabled') || scope.skipToggle) { + return; + } + + scope.$apply(function() { + // Toggle the checkbox value... + var viewValue = attr.ngChecked ? attr.checked : !ngModelCtrl.$viewValue; + + ngModelCtrl.$setViewValue(viewValue, ev && ev.type); + ngModelCtrl.$render(); + }); + } + + function render() { + // Cast the $viewValue to a boolean since it could be undefined + element.toggleClass('md-checked', !!ngModelCtrl.$viewValue && !isIndeterminate); + } + + function setIndeterminateState(newValue) { + isIndeterminate = newValue !== false; + if (isIndeterminate) { + element.attr('aria-checked', 'mixed'); + } + element.toggleClass('md-indeterminate', isIndeterminate); + } + } + } +} + +})(); +(function(){ +"use strict"; + +(function () { + "use strict"; + + /** + * Use a RegExp to check if the `md-colors=""` is static string + * or one that should be observed and dynamically interpolated. + */ + MdColorsDirective.$inject = ["$mdColors", "$mdUtil", "$log", "$parse"]; + MdColorsService.$inject = ["$mdTheming", "$mdUtil", "$log"]; + var STATIC_COLOR_EXPRESSION = /^{((\s|,)*?["'a-zA-Z-]+?\s*?:\s*?('|")[a-zA-Z0-9-.]*('|"))+\s*}$/; + var colorPalettes = null; + + /** + * @ngdoc module + * @name material.components.colors + * + * @description + * Define $mdColors service and a `md-colors=""` attribute directive + */ + angular + .module('material.components.colors', ['material.core']) + .directive('mdColors', MdColorsDirective) + .service('$mdColors', MdColorsService); + + /** + * @ngdoc service + * @name $mdColors + * @module material.components.colors + * + * @description + * With only defining themes, one couldn't get non ngMaterial elements colored with Material colors, + * `$mdColors` service is used by the md-color directive to convert the 1..n color expressions to RGBA values and will apply + * those values to element as CSS property values. + * + * @usage + * + * angular.controller('myCtrl', function ($mdColors) { + * var color = $mdColors.getThemeColor('myTheme-red-200-0.5'); + * ... + * }); + * + * + */ + function MdColorsService($mdTheming, $mdUtil, $log) { + colorPalettes = colorPalettes || Object.keys($mdTheming.PALETTES); + + // Publish service instance + return { + applyThemeColors: applyThemeColors, + getThemeColor: getThemeColor, + hasTheme: hasTheme + }; + + // ******************************************** + // Internal Methods + // ******************************************** + + /** + * @ngdoc method + * @name $mdColors#applyThemeColors + * + * @description + * Gets a color json object, keys are css properties and values are string of the wanted color + * Then calculate the rgba() values based on the theme color parts + * + * @param {DOMElement} element the element to apply the styles on. + * @param {object} colorExpression json object, keys are css properties and values are string of the wanted color, + * for example: `{color: 'red-A200-0.3'}`. + * + * @usage + * + * app.directive('myDirective', function($mdColors) { + * return { + * ... + * link: function (scope, elem) { + * $mdColors.applyThemeColors(elem, {color: 'red'}); + * } + * } + * }); + * + */ + function applyThemeColors(element, colorExpression) { + try { + if (colorExpression) { + // Assign the calculate RGBA color values directly as inline CSS + element.css(interpolateColors(colorExpression)); + } + } catch (e) { + $log.error(e.message); + } + + } + + /** + * @ngdoc method + * @name $mdColors#getThemeColor + * + * @description + * Get parsed color from expression + * + * @param {string} expression string of a color expression (for instance `'red-700-0.8'`) + * + * @returns {string} a css color expression (for instance `rgba(211, 47, 47, 0.8)`) + * + * @usage + * + * angular.controller('myCtrl', function ($mdColors) { + * var color = $mdColors.getThemeColor('myTheme-red-200-0.5'); + * ... + * }); + * + */ + function getThemeColor(expression) { + var color = extractColorOptions(expression); + + return parseColor(color); + } + + /** + * Return the parsed color + * @param color hashmap of color definitions + * @param contrast whether use contrast color for foreground + * @returns rgba color string + */ + function parseColor(color, contrast) { + contrast = contrast || false; + var rgbValues = $mdTheming.PALETTES[color.palette][color.hue]; + + rgbValues = contrast ? rgbValues.contrast : rgbValues.value; + + return $mdUtil.supplant('rgba({0}, {1}, {2}, {3})', + [rgbValues[0], rgbValues[1], rgbValues[2], rgbValues[3] || color.opacity] + ); + } + + /** + * Convert the color expression into an object with scope-interpolated values + * Then calculate the rgba() values based on the theme color parts + * + * @results Hashmap of CSS properties with associated `rgba( )` string vales + * + * + */ + function interpolateColors(themeColors) { + var rgbColors = {}; + + var hasColorProperty = themeColors.hasOwnProperty('color'); + + angular.forEach(themeColors, function (value, key) { + var color = extractColorOptions(value); + var hasBackground = key.indexOf('background') > -1; + + rgbColors[key] = parseColor(color); + if (hasBackground && !hasColorProperty) { + rgbColors.color = parseColor(color, true); + } + }); + + return rgbColors; + } + + /** + * Check if expression has defined theme + * e.g. + * 'myTheme-primary' => true + * 'red-800' => false + */ + function hasTheme(expression) { + return angular.isDefined($mdTheming.THEMES[expression.split('-')[0]]); + } + + /** + * For the evaluated expression, extract the color parts into a hash map + */ + function extractColorOptions(expression) { + var parts = expression.split('-'); + var hasTheme = angular.isDefined($mdTheming.THEMES[parts[0]]); + var theme = hasTheme ? parts.splice(0, 1)[0] : $mdTheming.defaultTheme(); + + return { + theme: theme, + palette: extractPalette(parts, theme), + hue: extractHue(parts, theme), + opacity: parts[2] || 1 + }; + } + + /** + * Calculate the theme palette name + */ + function extractPalette(parts, theme) { + // If the next section is one of the palettes we assume it's a two word palette + // Two word palette can be also written in camelCase, forming camelCase to dash-case + + var isTwoWord = parts.length > 1 && colorPalettes.indexOf(parts[1]) !== -1; + var palette = parts[0].replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); + + if (isTwoWord) palette = parts[0] + '-' + parts.splice(1, 1); + + if (colorPalettes.indexOf(palette) === -1) { + // If the palette is not in the palette list it's one of primary/accent/warn/background + var scheme = $mdTheming.THEMES[theme].colors[palette]; + if (!scheme) { + throw new Error($mdUtil.supplant('mdColors: couldn\'t find \'{palette}\' in the palettes.', {palette: palette})); + } + palette = scheme.name; + } + + return palette; + } + + function extractHue(parts, theme) { + var themeColors = $mdTheming.THEMES[theme].colors; + + if (parts[1] === 'hue') { + var hueNumber = parseInt(parts.splice(2, 1)[0], 10); + + if (hueNumber < 1 || hueNumber > 3) { + throw new Error($mdUtil.supplant('mdColors: \'hue-{hueNumber}\' is not a valid hue, can be only \'hue-1\', \'hue-2\' and \'hue-3\'', {hueNumber: hueNumber})); + } + parts[1] = 'hue-' + hueNumber; + + if (!(parts[0] in themeColors)) { + throw new Error($mdUtil.supplant('mdColors: \'hue-x\' can only be used with [{availableThemes}], but was used with \'{usedTheme}\'', { + availableThemes: Object.keys(themeColors).join(', '), + usedTheme: parts[0] + })); + } + + return themeColors[parts[0]].hues[parts[1]]; + } + + return parts[1] || themeColors[parts[0] in themeColors ? parts[0] : 'primary'].hues['default']; + } + } + + /** + * @ngdoc directive + * @name mdColors + * @module material.components.colors + * + * @restrict A + * + * @description + * `mdColors` directive will apply the theme-based color expression as RGBA CSS style values. + * + * The format will be similar to our color defining in the scss files: + * + * ## `[?theme]-[palette]-[?hue]-[?opacity]` + * - [theme] - default value is the default theme + * - [palette] - can be either palette name or primary/accent/warn/background + * - [hue] - default is 500 (hue-x can be used with primary/accent/warn/background) + * - [opacity] - default is 1 + * + * > `?` indicates optional parameter + * + * @usage + * + *
      + *
      + * Color demo + *
      + *
      + *
      + * + * `mdColors` directive will automatically watch for changes in the expression if it recognizes an interpolation + * expression or a function. For performance options, you can use `::` prefix to the `md-colors` expression + * to indicate a one-time data binding. + * + * + * + * + * + */ + function MdColorsDirective($mdColors, $mdUtil, $log, $parse) { + return { + restrict: 'A', + require: ['^?mdTheme'], + compile: function (tElem, tAttrs) { + var shouldWatch = shouldColorsWatch(); + + return function (scope, element, attrs, ctrl) { + var mdThemeController = ctrl[0]; + + var lastColors = {}; + + var parseColors = function (theme) { + if (typeof theme !== 'string') { + theme = ''; + } + + if (!attrs.mdColors) { + attrs.mdColors = '{}'; + } + + /** + * Json.parse() does not work because the keys are not quoted; + * use $parse to convert to a hash map + */ + var colors = $parse(attrs.mdColors)(scope); + + /** + * If mdTheme is defined up the DOM tree + * we add mdTheme theme to colors who doesn't specified a theme + * + * # example + * + *
      + *
      + * Color demo + *
      + *
      + *
      + * + * 'primary-600' will be 'myTheme-primary-600', + * but 'mySecondTheme-accent-200' will stay the same cause it has a theme prefix + */ + if (mdThemeController) { + Object.keys(colors).forEach(function (prop) { + var color = colors[prop]; + if (!$mdColors.hasTheme(color)) { + colors[prop] = (theme || mdThemeController.$mdTheme) + '-' + color; + } + }); + } + + cleanElement(colors); + + return colors; + }; + + var cleanElement = function (colors) { + if (!angular.equals(colors, lastColors)) { + var keys = Object.keys(lastColors); + + if (lastColors.background && !keys.color) { + keys.push('color'); + } + + keys.forEach(function (key) { + element.css(key, ''); + }); + } + + lastColors = colors; + }; + + /** + * Registering for mgTheme changes and asking mdTheme controller run our callback whenever a theme changes + */ + var unregisterChanges = angular.noop; + + if (mdThemeController) { + unregisterChanges = mdThemeController.registerChanges(function (theme) { + $mdColors.applyThemeColors(element, parseColors(theme)); + }); + } + + scope.$on('$destroy', function () { + unregisterChanges(); + }); + + try { + if (shouldWatch) { + scope.$watch(parseColors, angular.bind(this, + $mdColors.applyThemeColors, element + ), true); + } + else { + $mdColors.applyThemeColors(element, parseColors()); + } + + } + catch (e) { + $log.error(e.message); + } + + }; + + function shouldColorsWatch() { + // Simulate 1x binding and mark mdColorsWatch == false + var rawColorExpression = tAttrs.mdColors; + var bindOnce = rawColorExpression.indexOf('::') > -1; + var isStatic = bindOnce ? true : STATIC_COLOR_EXPRESSION.test(tAttrs.mdColors); + + // Remove it for the postLink... + tAttrs.mdColors = rawColorExpression.replace('::', ''); + + var hasWatchAttr = angular.isDefined(tAttrs.mdColorsWatch); + + return (bindOnce || isStatic) ? false : + hasWatchAttr ? $mdUtil.parseAttributeBoolean(tAttrs.mdColorsWatch) : true; + } + } + }; + + } + + +})(); + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.components.content + * + * @description + * Scrollable content + */ +mdContentDirective.$inject = ["$mdTheming"]; +angular.module('material.components.content', [ + 'material.core' +]) + .directive('mdContent', mdContentDirective); + +/** + * @ngdoc directive + * @name mdContent + * @module material.components.content + * + * @restrict E + * + * @description + * + * The `` directive is a container element useful for scrollable content. It achieves + * this by setting the CSS `overflow` property to `auto` so that content can properly scroll. + * + * In general, `` components are not designed to be nested inside one another. If + * possible, it is better to make them siblings. This often results in a better user experience as + * having nested scrollbars may confuse the user. + * + * ## Troubleshooting + * + * In some cases, you may wish to apply the `md-no-momentum` class to ensure that Safari's + * momentum scrolling is disabled. Momentum scrolling can cause flickering issues while scrolling + * SVG icons and some other components. + * + * Additionally, we now also offer the `md-no-flicker` class which can be applied to any element + * and uses a Webkit-specific filter of `blur(0px)` that forces GPU rendering of all elements + * inside (which eliminates the flicker on iOS devices). + * + * _Note: Forcing an element to render on the GPU can have unintended side-effects, especially + * related to the z-index of elements. Please use with caution and only on the elements needed._ + * + * @usage + * + * Add the `[layout-padding]` attribute to make the content padded. + * + * + * + * Lorem ipsum dolor sit amet, ne quod novum mei. + * + * + */ + +function mdContentDirective($mdTheming) { + return { + restrict: 'E', + controller: ['$scope', '$element', ContentController], + link: function(scope, element) { + element.addClass('_md'); // private md component indicator for styling + + $mdTheming(element); + scope.$broadcast('$mdContentLoaded', element); + + iosScrollFix(element[0]); + } + }; + + function ContentController($scope, $element) { + this.$scope = $scope; + this.$element = $element; + } +} + +function iosScrollFix(node) { + // IOS FIX: + // If we scroll where there is no more room for the webview to scroll, + // by default the webview itself will scroll up and down, this looks really + // bad. So if we are scrolling to the very top or bottom, add/subtract one + angular.element(node).on('$md.pressdown', function(ev) { + // Only touch events + if (ev.pointer.type !== 't') return; + // Don't let a child content's touchstart ruin it for us. + if (ev.$materialScrollFixed) return; + ev.$materialScrollFixed = true; + + if (node.scrollTop === 0) { + node.scrollTop = 1; + } else if (node.scrollHeight === node.scrollTop + node.offsetHeight) { + node.scrollTop -= 1; + } + }); +} + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.components.datepicker + * @description Module for the datepicker component. + */ + +angular.module('material.components.datepicker', [ + 'material.core', + 'material.components.icon', + 'material.components.virtualRepeat' +]); + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.components.dialog + */ +MdDialogDirective.$inject = ["$$rAF", "$mdTheming", "$mdDialog"]; +MdDialogProvider.$inject = ["$$interimElementProvider"]; +angular + .module('material.components.dialog', [ + 'material.core', + 'material.components.backdrop' + ]) + .directive('mdDialog', MdDialogDirective) + .provider('$mdDialog', MdDialogProvider); + +/** + * @ngdoc directive + * @name mdDialog + * @module material.components.dialog + * + * @restrict E + * + * @description + * `` - The dialog's template must be inside this element. + * + * Inside, use an `` element for the dialog's content, and use + * an `` element for the dialog's actions. + * + * ## CSS + * - `.md-dialog-content` - class that sets the padding on the content as the spec file + * + * ## Notes + * - If you specify an `id` for the ``, the `` will have the same `id` + * prefixed with `dialogContent_`. + * + * @usage + * ### Dialog template + * + * + * + * + * + *

      Number {{item}}

      + *
      + *
      + *
      + * + * Close Dialog + * + *
      + *
      + */ +function MdDialogDirective($$rAF, $mdTheming, $mdDialog) { + return { + restrict: 'E', + link: function(scope, element) { + element.addClass('_md'); // private md component indicator for styling + + $mdTheming(element); + $$rAF(function() { + var images; + var content = element[0].querySelector('md-dialog-content'); + + if (content) { + images = content.getElementsByTagName('img'); + addOverflowClass(); + //-- delayed image loading may impact scroll height, check after images are loaded + angular.element(images).on('load', addOverflowClass); + } + + scope.$on('$destroy', function() { + $mdDialog.destroy(element); + }); + + /** + * + */ + function addOverflowClass() { + element.toggleClass('md-content-overflow', content.scrollHeight > content.clientHeight); + } + + + }); + } + }; +} + +/** + * @ngdoc service + * @name $mdDialog + * @module material.components.dialog + * + * @description + * `$mdDialog` opens a dialog over the app to inform users about critical information or require + * them to make decisions. There are two approaches for setup: a simple promise API + * and regular object syntax. + * + * ## Restrictions + * + * - The dialog is always given an isolate scope. + * - The dialog's template must have an outer `` element. + * Inside, use an `` element for the dialog's content, and use + * an `` element for the dialog's actions. + * - Dialogs must cover the entire application to keep interactions inside of them. + * Use the `parent` option to change where dialogs are appended. + * + * ## Sizing + * - Complex dialogs can be sized with `flex="percentage"`, i.e. `flex="66"`. + * - Default max-width is 80% of the `rootElement` or `parent`. + * + * ## CSS + * - `.md-dialog-content` - class that sets the padding on the content as the spec file + * + * @usage + * + *
      + *
      + * + * Employee Alert! + * + *
      + *
      + * + * Custom Dialog + * + *
      + *
      + * + * Close Alert + * + *
      + *
      + * + * Greet Employee + * + *
      + *
      + *
      + * + * ### JavaScript: object syntax + * + * (function(angular, undefined){ + * "use strict"; + * + * angular + * .module('demoApp', ['ngMaterial']) + * .controller('AppCtrl', AppController); + * + * function AppController($scope, $mdDialog) { + * var alert; + * $scope.showAlert = showAlert; + * $scope.showDialog = showDialog; + * $scope.items = [1, 2, 3]; + * + * // Internal method + * function showAlert() { + * alert = $mdDialog.alert({ + * title: 'Attention', + * textContent: 'This is an example of how easy dialogs can be!', + * ok: 'Close' + * }); + * + * $mdDialog + * .show( alert ) + * .finally(function() { + * alert = undefined; + * }); + * } + * + * function showDialog($event) { + * var parentEl = angular.element(document.body); + * $mdDialog.show({ + * parent: parentEl, + * targetEvent: $event, + * template: + * '' + + * ' '+ + * ' '+ + * ' '+ + * '

      Number {{item}}

      ' + + * ' '+ + * '
      '+ + * '
      ' + + * ' ' + + * ' ' + + * ' Close Dialog' + + * ' ' + + * ' ' + + * '
      ', + * locals: { + * items: $scope.items + * }, + * controller: DialogController + * }); + * function DialogController($scope, $mdDialog, items) { + * $scope.items = items; + * $scope.closeDialog = function() { + * $mdDialog.hide(); + * } + * } + * } + * } + * })(angular); + *
      + * + * ### Pre-Rendered Dialogs + * By using the `contentElement` option, it is possible to use an already existing element in the DOM. + * + * + * $scope.showPrerenderedDialog = function() { + * $mdDialog.show({ + * contentElement: '#myStaticDialog', + * parent: angular.element(document.body) + * }); + * }; + * + * + * When using a string as value, `$mdDialog` will automatically query the DOM for the specified CSS selector. + * + * + *
      + *
      + * + * This is a pre-rendered dialog. + * + *
      + *
      + *
      + * + * **Notice**: It is important, to use the `.md-dialog-container` as the content element, otherwise the dialog + * will not show up. + * + * It also possible to use a DOM element for the `contentElement` option. + * - `contentElement: document.querySelector('#myStaticDialog')` + * - `contentElement: angular.element(TEMPLATE)` + * + * When using a `template` as content element, it will be not compiled upon open. + * This allows you to compile the element yourself and use it each time the dialog opens. + * + * ### Custom Presets + * Developers are also able to create their own preset, which can be easily used without repeating + * their options each time. + * + * + * $mdDialogProvider.addPreset('testPreset', { + * options: function() { + * return { + * template: + * '' + + * 'This is a custom preset' + + * '', + * controllerAs: 'dialog', + * bindToController: true, + * clickOutsideToClose: true, + * escapeToClose: true + * }; + * } + * }); + * + * + * After you created your preset at config phase, you can easily access it. + * + * + * $mdDialog.show( + * $mdDialog.testPreset() + * ); + * + * + * ### JavaScript: promise API syntax, custom dialog template + * + * (function(angular, undefined){ + * "use strict"; + * + * angular + * .module('demoApp', ['ngMaterial']) + * .controller('EmployeeController', EmployeeEditor) + * .controller('GreetingController', GreetingController); + * + * // Fictitious Employee Editor to show how to use simple and complex dialogs. + * + * function EmployeeEditor($scope, $mdDialog) { + * var alert; + * + * $scope.showAlert = showAlert; + * $scope.closeAlert = closeAlert; + * $scope.showGreeting = showCustomGreeting; + * + * $scope.hasAlert = function() { return !!alert }; + * $scope.userName = $scope.userName || 'Bobby'; + * + * // Dialog #1 - Show simple alert dialog and cache + * // reference to dialog instance + * + * function showAlert() { + * alert = $mdDialog.alert() + * .title('Attention, ' + $scope.userName) + * .textContent('This is an example of how easy dialogs can be!') + * .ok('Close'); + * + * $mdDialog + * .show( alert ) + * .finally(function() { + * alert = undefined; + * }); + * } + * + * // Close the specified dialog instance and resolve with 'finished' flag + * // Normally this is not needed, just use '$mdDialog.hide()' to close + * // the most recent dialog popup. + * + * function closeAlert() { + * $mdDialog.hide( alert, "finished" ); + * alert = undefined; + * } + * + * // Dialog #2 - Demonstrate more complex dialogs construction and popup. + * + * function showCustomGreeting($event) { + * $mdDialog.show({ + * targetEvent: $event, + * template: + * '' + + * + * ' Hello {{ employee }}!' + + * + * ' ' + + * ' ' + + * ' Close Greeting' + + * ' ' + + * ' ' + + * '', + * controller: 'GreetingController', + * onComplete: afterShowAnimation, + * locals: { employee: $scope.userName } + * }); + * + * // When the 'enter' animation finishes... + * + * function afterShowAnimation(scope, element, options) { + * // post-show code here: DOM element focus, etc. + * } + * } + * + * // Dialog #3 - Demonstrate use of ControllerAs and passing $scope to dialog + * // Here we used ng-controller="GreetingController as vm" and + * // $scope.vm === + * + * function showCustomGreeting() { + * + * $mdDialog.show({ + * clickOutsideToClose: true, + * + * scope: $scope, // use parent scope in template + * preserveScope: true, // do not forget this if use parent scope + + * // Since GreetingController is instantiated with ControllerAs syntax + * // AND we are passing the parent '$scope' to the dialog, we MUST + * // use 'vm.' in the template markup + * + * template: '' + + * ' ' + + * ' Hi There {{vm.employee}}' + + * ' ' + + * '', + * + * controller: function DialogController($scope, $mdDialog) { + * $scope.closeDialog = function() { + * $mdDialog.hide(); + * } + * } + * }); + * } + * + * } + * + * // Greeting controller used with the more complex 'showCustomGreeting()' custom dialog + * + * function GreetingController($scope, $mdDialog, employee) { + * // Assigned from construction locals options... + * $scope.employee = employee; + * + * $scope.closeDialog = function() { + * // Easily hides most recent dialog shown... + * // no specific instance reference is needed. + * $mdDialog.hide(); + * }; + * } + * + * })(angular); + * + */ + +/** + * @ngdoc method + * @name $mdDialog#alert + * + * @description + * Builds a preconfigured dialog with the specified message. + * + * @returns {obj} an `$mdDialogPreset` with the chainable configuration methods: + * + * - $mdDialogPreset#title(string) - Sets the alert title. + * - $mdDialogPreset#textContent(string) - Sets the alert message. + * - $mdDialogPreset#htmlContent(string) - Sets the alert message as HTML. Requires ngSanitize + * module to be loaded. HTML is not run through Angular's compiler. + * - $mdDialogPreset#ok(string) - Sets the alert "Okay" button text. + * - $mdDialogPreset#theme(string) - Sets the theme of the alert dialog. + * - $mdDialogPreset#targetEvent(DOMClickEvent=) - A click's event object. When passed in as an option, + * the location of the click will be used as the starting point for the opening animation + * of the the dialog. + * + */ + +/** + * @ngdoc method + * @name $mdDialog#confirm + * + * @description + * Builds a preconfigured dialog with the specified message. You can call show and the promise returned + * will be resolved only if the user clicks the confirm action on the dialog. + * + * @returns {obj} an `$mdDialogPreset` with the chainable configuration methods: + * + * Additionally, it supports the following methods: + * + * - $mdDialogPreset#title(string) - Sets the confirm title. + * - $mdDialogPreset#textContent(string) - Sets the confirm message. + * - $mdDialogPreset#htmlContent(string) - Sets the confirm message as HTML. Requires ngSanitize + * module to be loaded. HTML is not run through Angular's compiler. + * - $mdDialogPreset#ok(string) - Sets the confirm "Okay" button text. + * - $mdDialogPreset#cancel(string) - Sets the confirm "Cancel" button text. + * - $mdDialogPreset#theme(string) - Sets the theme of the confirm dialog. + * - $mdDialogPreset#targetEvent(DOMClickEvent=) - A click's event object. When passed in as an option, + * the location of the click will be used as the starting point for the opening animation + * of the the dialog. + * + */ + +/** + * @ngdoc method + * @name $mdDialog#prompt + * + * @description + * Builds a preconfigured dialog with the specified message and input box. You can call show and the promise returned + * will be resolved only if the user clicks the prompt action on the dialog, passing the input value as the first argument. + * + * @returns {obj} an `$mdDialogPreset` with the chainable configuration methods: + * + * Additionally, it supports the following methods: + * + * - $mdDialogPreset#title(string) - Sets the prompt title. + * - $mdDialogPreset#textContent(string) - Sets the prompt message. + * - $mdDialogPreset#htmlContent(string) - Sets the prompt message as HTML. Requires ngSanitize + * module to be loaded. HTML is not run through Angular's compiler. + * - $mdDialogPreset#placeholder(string) - Sets the placeholder text for the input. + * - $mdDialogPreset#initialValue(string) - Sets the initial value for the prompt input. + * - $mdDialogPreset#ok(string) - Sets the prompt "Okay" button text. + * - $mdDialogPreset#cancel(string) - Sets the prompt "Cancel" button text. + * - $mdDialogPreset#theme(string) - Sets the theme of the prompt dialog. + * - $mdDialogPreset#targetEvent(DOMClickEvent=) - A click's event object. When passed in as an option, + * the location of the click will be used as the starting point for the opening animation + * of the the dialog. + * + */ + +/** + * @ngdoc method + * @name $mdDialog#show + * + * @description + * Show a dialog with the specified options. + * + * @param {object} optionsOrPreset Either provide an `$mdDialogPreset` returned from `alert()`, and + * `confirm()`, or an options object with the following properties: + * - `templateUrl` - `{string=}`: The url of a template that will be used as the content + * of the dialog. + * - `template` - `{string=}`: HTML template to show in the dialog. This **must** be trusted HTML + * with respect to Angular's [$sce service](https://docs.angularjs.org/api/ng/service/$sce). + * This template should **never** be constructed with any kind of user input or user data. + * - `contentElement` - `{string|Element}`: Instead of using a template, which will be compiled each time a + * dialog opens, you can also use a DOM element.
      + * * When specifying an element, which is present on the DOM, `$mdDialog` will temporary fetch the element into + * the dialog and restores it at the old DOM position upon close. + * * When specifying a string, the string be used as a CSS selector, to lookup for the element in the DOM. + * - `autoWrap` - `{boolean=}`: Whether or not to automatically wrap the template with a + * `` tag if one is not provided. Defaults to true. Can be disabled if you provide a + * custom dialog directive. + * - `targetEvent` - `{DOMClickEvent=}`: A click's event object. When passed in as an option, + * the location of the click will be used as the starting point for the opening animation + * of the the dialog. + * - `openFrom` - `{string|Element|object}`: The query selector, DOM element or the Rect object + * that is used to determine the bounds (top, left, height, width) from which the Dialog will + * originate. + * - `closeTo` - `{string|Element|object}`: The query selector, DOM element or the Rect object + * that is used to determine the bounds (top, left, height, width) to which the Dialog will + * target. + * - `scope` - `{object=}`: the scope to link the template / controller to. If none is specified, + * it will create a new isolate scope. + * This scope will be destroyed when the dialog is removed unless `preserveScope` is set to true. + * - `preserveScope` - `{boolean=}`: whether to preserve the scope when the element is removed. Default is false + * - `disableParentScroll` - `{boolean=}`: Whether to disable scrolling while the dialog is open. + * Default true. + * - `hasBackdrop` - `{boolean=}`: Whether there should be an opaque backdrop behind the dialog. + * Default true. + * - `clickOutsideToClose` - `{boolean=}`: Whether the user can click outside the dialog to + * close it. Default false. + * - `escapeToClose` - `{boolean=}`: Whether the user can press escape to close the dialog. + * Default true. + * - `focusOnOpen` - `{boolean=}`: An option to override focus behavior on open. Only disable if + * focusing some other way, as focus management is required for dialogs to be accessible. + * Defaults to true. + * - `controller` - `{function|string=}`: The controller to associate with the dialog. The controller + * will be injected with the local `$mdDialog`, which passes along a scope for the dialog. + * - `locals` - `{object=}`: An object containing key/value pairs. The keys will be used as names + * of values to inject into the controller. For example, `locals: {three: 3}` would inject + * `three` into the controller, with the value 3. If `bindToController` is true, they will be + * copied to the controller instead. + * - `bindToController` - `bool`: bind the locals to the controller, instead of passing them in. + * - `resolve` - `{object=}`: Similar to locals, except it takes promises as values, and the + * dialog will not open until all of the promises resolve. + * - `controllerAs` - `{string=}`: An alias to assign the controller to on the scope. + * - `parent` - `{element=}`: The element to append the dialog to. Defaults to appending + * to the root element of the application. + * - `onShowing` - `function(scope, element)`: Callback function used to announce the show() action is + * starting. + * - `onComplete` - `function(scope, element)`: Callback function used to announce when the show() action is + * finished. + * - `onRemoving` - `function(element, removePromise)`: Callback function used to announce the + * close/hide() action is starting. This allows developers to run custom animations + * in parallel the close animations. + * - `fullscreen` `{boolean=}`: An option to toggle whether the dialog should show in fullscreen + * or not. Defaults to `false`. + * @returns {promise} A promise that can be resolved with `$mdDialog.hide()` or + * rejected with `$mdDialog.cancel()`. + */ + +/** + * @ngdoc method + * @name $mdDialog#hide + * + * @description + * Hide an existing dialog and resolve the promise returned from `$mdDialog.show()`. + * + * @param {*=} response An argument for the resolved promise. + * + * @returns {promise} A promise that is resolved when the dialog has been closed. + */ + +/** + * @ngdoc method + * @name $mdDialog#cancel + * + * @description + * Hide an existing dialog and reject the promise returned from `$mdDialog.show()`. + * + * @param {*=} response An argument for the rejected promise. + * + * @returns {promise} A promise that is resolved when the dialog has been closed. + */ + +function MdDialogProvider($$interimElementProvider) { + // Elements to capture and redirect focus when the user presses tab at the dialog boundary. + advancedDialogOptions.$inject = ["$mdDialog", "$mdConstant"]; + dialogDefaultOptions.$inject = ["$mdDialog", "$mdAria", "$mdUtil", "$mdConstant", "$animate", "$document", "$window", "$rootElement", "$log", "$injector", "$mdTheming"]; + var topFocusTrap, bottomFocusTrap; + + return $$interimElementProvider('$mdDialog') + .setDefaults({ + methods: ['disableParentScroll', 'hasBackdrop', 'clickOutsideToClose', 'escapeToClose', + 'targetEvent', 'closeTo', 'openFrom', 'parent', 'fullscreen', 'contentElement'], + options: dialogDefaultOptions + }) + .addPreset('alert', { + methods: ['title', 'htmlContent', 'textContent', 'content', 'ariaLabel', 'ok', 'theme', + 'css'], + options: advancedDialogOptions + }) + .addPreset('confirm', { + methods: ['title', 'htmlContent', 'textContent', 'content', 'ariaLabel', 'ok', 'cancel', + 'theme', 'css'], + options: advancedDialogOptions + }) + .addPreset('prompt', { + methods: ['title', 'htmlContent', 'textContent', 'initialValue', 'content', 'placeholder', 'ariaLabel', + 'ok', 'cancel', 'theme', 'css'], + options: advancedDialogOptions + }); + + /* @ngInject */ + function advancedDialogOptions($mdDialog, $mdConstant) { + return { + template: [ + '', + ' ', + '

      {{ dialog.title }}

      ', + '
      ', + '
      ', + '

      {{::dialog.mdTextContent}}

      ', + '
      ', + ' ', + ' ', + ' ', + '
      ', + ' ', + ' ', + ' {{ dialog.cancel }}', + ' ', + ' ', + ' {{ dialog.ok }}', + ' ', + ' ', + '
      ' + ].join('').replace(/\s\s+/g, ''), + controller: function mdDialogCtrl() { + var isPrompt = this.$type == 'prompt'; + + if (isPrompt && this.initialValue) { + this.result = this.initialValue; + } + + this.hide = function() { + $mdDialog.hide(isPrompt ? this.result : true); + }; + this.abort = function() { + $mdDialog.cancel(); + }; + this.keypress = function($event) { + if ($event.keyCode === $mdConstant.KEY_CODE.ENTER) { + $mdDialog.hide(this.result); + } + }; + }, + controllerAs: 'dialog', + bindToController: true, + }; + } + + /* @ngInject */ + function dialogDefaultOptions($mdDialog, $mdAria, $mdUtil, $mdConstant, $animate, $document, $window, $rootElement, + $log, $injector, $mdTheming) { + + return { + hasBackdrop: true, + isolateScope: true, + onCompiling: beforeCompile, + onShow: onShow, + onShowing: beforeShow, + onRemove: onRemove, + clickOutsideToClose: false, + escapeToClose: true, + targetEvent: null, + contentElement: null, + closeTo: null, + openFrom: null, + focusOnOpen: true, + disableParentScroll: true, + autoWrap: true, + fullscreen: false, + transformTemplate: function(template, options) { + // Make the dialog container focusable, because otherwise the focus will be always redirected to + // an element outside of the container, and the focus trap won't work probably.. + // Also the tabindex is needed for the `escapeToClose` functionality, because + // the keyDown event can't be triggered when the focus is outside of the container. + return '
      ' + validatedTemplate(template) + '
      '; + + /** + * The specified template should contain a wrapper element.... + */ + function validatedTemplate(template) { + if (options.autoWrap && !/<\/md-dialog>/g.test(template)) { + return '' + (template || '') + ''; + } else { + return template || ''; + } + } + } + }; + + function beforeCompile(options) { + // Automatically apply the theme, if the user didn't specify a theme explicitly. + // Those option changes need to be done, before the compilation has started, because otherwise + // the option changes will be not available in the $mdCompilers locales. + detectTheming(options); + + if (options.contentElement) { + options.restoreContentElement = installContentElement(options); + } + } + + function beforeShow(scope, element, options, controller) { + + if (controller) { + controller.mdHtmlContent = controller.htmlContent || options.htmlContent || ''; + controller.mdTextContent = controller.textContent || options.textContent || + controller.content || options.content || ''; + + if (controller.mdHtmlContent && !$injector.has('$sanitize')) { + throw Error('The ngSanitize module must be loaded in order to use htmlContent.'); + } + + if (controller.mdHtmlContent && controller.mdTextContent) { + throw Error('md-dialog cannot have both `htmlContent` and `textContent`'); + } + } + } + + /** Show method for dialogs */ + function onShow(scope, element, options, controller) { + angular.element($document[0].body).addClass('md-dialog-is-showing'); + + var dialogElement = element.find('md-dialog'); + + // Once a dialog has `ng-cloak` applied on his template the dialog animation will not work properly. + // This is a very common problem, so we have to notify the developer about this. + if (dialogElement.hasClass('ng-cloak')) { + var message = '$mdDialog: using `` will affect the dialog opening animations.'; + $log.warn( message, element[0] ); + } + + captureParentAndFromToElements(options); + configureAria(dialogElement, options); + showBackdrop(scope, element, options); + activateListeners(element, options); + + return dialogPopIn(element, options) + .then(function() { + lockScreenReader(element, options); + warnDeprecatedActions(); + focusOnOpen(); + }); + + /** + * Check to see if they used the deprecated .md-actions class and log a warning + */ + function warnDeprecatedActions() { + if (element[0].querySelector('.md-actions')) { + $log.warn('Using a class of md-actions is deprecated, please use .'); + } + } + + /** + * For alerts, focus on content... otherwise focus on + * the close button (or equivalent) + */ + function focusOnOpen() { + if (options.focusOnOpen) { + var target = $mdUtil.findFocusTarget(element) || findCloseButton() || dialogElement; + target.focus(); + } + + /** + * If no element with class dialog-close, try to find the last + * button child in md-actions and assume it is a close button. + * + * If we find no actions at all, log a warning to the console. + */ + function findCloseButton() { + var closeButton = element[0].querySelector('.dialog-close'); + + if (!closeButton) { + var actionButtons = element[0].querySelectorAll('.md-actions button, md-dialog-actions button'); + closeButton = actionButtons[actionButtons.length - 1]; + } + + return closeButton; + } + } + } + + /** + * Remove function for all dialogs + */ + function onRemove(scope, element, options) { + options.deactivateListeners(); + options.unlockScreenReader(); + options.hideBackdrop(options.$destroy); + + // Remove the focus traps that we added earlier for keeping focus within the dialog. + if (topFocusTrap && topFocusTrap.parentNode) { + topFocusTrap.parentNode.removeChild(topFocusTrap); + } + + if (bottomFocusTrap && bottomFocusTrap.parentNode) { + bottomFocusTrap.parentNode.removeChild(bottomFocusTrap); + } + + // For navigation $destroy events, do a quick, non-animated removal, + // but for normal closes (from clicks, etc) animate the removal + return !!options.$destroy ? detachAndClean() : animateRemoval().then( detachAndClean ); + + /** + * For normal closes, animate the removal. + * For forced closes (like $destroy events), skip the animations + */ + function animateRemoval() { + return dialogPopOut(element, options); + } + + /** + * Detach the element + */ + function detachAndClean() { + angular.element($document[0].body).removeClass('md-dialog-is-showing'); + // Only remove the element, if it's not provided through the contentElement option. + if (!options.contentElement) { + element.remove(); + } else { + options.reverseContainerStretch(); + options.restoreContentElement(); + } + + if (!options.$destroy) options.origin.focus(); + } + } + + function detectTheming(options) { + // Only detect the theming, if the developer didn't specify the theme specifically. + if (options.theme) return; + + options.theme = $mdTheming.defaultTheme(); + + if (options.targetEvent && options.targetEvent.target) { + var targetEl = angular.element(options.targetEvent.target); + + // Once the user specifies a targetEvent, we will automatically try to find the correct + // nested theme. + options.theme = (targetEl.controller('mdTheme') || {}).$mdTheme || options.theme; + } + + } + + /** + * Installs a content element to the current $$interimElement provider options. + * @returns {Function} Function to restore the content element at its old DOM location. + */ + function installContentElement(options) { + var contentEl = options.contentElement; + var restoreFn = null; + + if (angular.isString(contentEl)) { + contentEl = document.querySelector(contentEl); + restoreFn = createRestoreFn(contentEl); + } else { + contentEl = contentEl[0] || contentEl; + + // When the element is visible in the DOM, then we restore it at close of the dialog. + // Otherwise it will be removed from the DOM after close. + if (document.contains(contentEl)) { + restoreFn = createRestoreFn(contentEl); + } else { + restoreFn = function() { + contentEl.parentNode.removeChild(contentEl); + } + } + } + + // Overwrite the options to use the content element. + options.element = angular.element(contentEl); + options.skipCompile = true; + + return restoreFn; + + function createRestoreFn(element) { + var parent = element.parentNode; + var nextSibling = element.nextElementSibling; + + return function() { + if (!nextSibling) { + // When the element didn't had any sibling, then it can be simply appended to the + // parent, because it plays no role, which index it had before. + parent.appendChild(element); + } else { + // When the element had a sibling, which marks the previous position of the element + // in the DOM, we insert it correctly before the sibling, to have the same index as + // before. + parent.insertBefore(element, nextSibling); + } + } + } + } + + /** + * Capture originator/trigger/from/to element information (if available) + * and the parent container for the dialog; defaults to the $rootElement + * unless overridden in the options.parent + */ + function captureParentAndFromToElements(options) { + options.origin = angular.extend({ + element: null, + bounds: null, + focus: angular.noop + }, options.origin || {}); + + options.parent = getDomElement(options.parent, $rootElement); + options.closeTo = getBoundingClientRect(getDomElement(options.closeTo)); + options.openFrom = getBoundingClientRect(getDomElement(options.openFrom)); + + if ( options.targetEvent ) { + options.origin = getBoundingClientRect(options.targetEvent.target, options.origin); + } + + + /** + * Identify the bounding RECT for the target element + * + */ + function getBoundingClientRect (element, orig) { + var source = angular.element((element || {})); + if (source && source.length) { + // Compute and save the target element's bounding rect, so that if the + // element is hidden when the dialog closes, we can shrink the dialog + // back to the same position it expanded from. + // + // Checking if the source is a rect object or a DOM element + var bounds = {top:0,left:0,height:0,width:0}; + var hasFn = angular.isFunction(source[0].getBoundingClientRect); + + return angular.extend(orig || {}, { + element : hasFn ? source : undefined, + bounds : hasFn ? source[0].getBoundingClientRect() : angular.extend({}, bounds, source[0]), + focus : angular.bind(source, source.focus), + }); + } + } + + /** + * If the specifier is a simple string selector, then query for + * the DOM element. + */ + function getDomElement(element, defaultElement) { + if (angular.isString(element)) { + element = $document[0].querySelector(element); + } + + // If we have a reference to a raw dom element, always wrap it in jqLite + return angular.element(element || defaultElement); + } + + } + + /** + * Listen for escape keys and outside clicks to auto close + */ + function activateListeners(element, options) { + var window = angular.element($window); + var onWindowResize = $mdUtil.debounce(function() { + stretchDialogContainerToViewport(element, options); + }, 60); + + var removeListeners = []; + var smartClose = function() { + // Only 'confirm' dialogs have a cancel button... escape/clickOutside will + // cancel or fallback to hide. + var closeFn = ( options.$type == 'alert' ) ? $mdDialog.hide : $mdDialog.cancel; + $mdUtil.nextTick(closeFn, true); + }; + + if (options.escapeToClose) { + var parentTarget = options.parent; + var keyHandlerFn = function(ev) { + if (ev.keyCode === $mdConstant.KEY_CODE.ESCAPE) { + ev.stopPropagation(); + ev.preventDefault(); + + smartClose(); + } + }; + + // Add keydown listeners + element.on('keydown', keyHandlerFn); + parentTarget.on('keydown', keyHandlerFn); + + // Queue remove listeners function + removeListeners.push(function() { + + element.off('keydown', keyHandlerFn); + parentTarget.off('keydown', keyHandlerFn); + + }); + } + + // Register listener to update dialog on window resize + window.on('resize', onWindowResize); + + removeListeners.push(function() { + window.off('resize', onWindowResize); + }); + + if (options.clickOutsideToClose) { + var target = element; + var sourceElem; + + // Keep track of the element on which the mouse originally went down + // so that we can only close the backdrop when the 'click' started on it. + // A simple 'click' handler does not work, + // it sets the target object as the element the mouse went down on. + var mousedownHandler = function(ev) { + sourceElem = ev.target; + }; + + // We check if our original element and the target is the backdrop + // because if the original was the backdrop and the target was inside the dialog + // we don't want to dialog to close. + var mouseupHandler = function(ev) { + if (sourceElem === target[0] && ev.target === target[0]) { + ev.stopPropagation(); + ev.preventDefault(); + + smartClose(); + } + }; + + // Add listeners + target.on('mousedown', mousedownHandler); + target.on('mouseup', mouseupHandler); + + // Queue remove listeners function + removeListeners.push(function() { + target.off('mousedown', mousedownHandler); + target.off('mouseup', mouseupHandler); + }); + } + + // Attach specific `remove` listener handler + options.deactivateListeners = function() { + removeListeners.forEach(function(removeFn) { + removeFn(); + }); + options.deactivateListeners = null; + }; + } + + /** + * Show modal backdrop element... + */ + function showBackdrop(scope, element, options) { + + if (options.disableParentScroll) { + // !! DO this before creating the backdrop; since disableScrollAround() + // configures the scroll offset; which is used by mdBackDrop postLink() + options.restoreScroll = $mdUtil.disableScrollAround(element, options.parent); + } + + if (options.hasBackdrop) { + options.backdrop = $mdUtil.createBackdrop(scope, "md-dialog-backdrop md-opaque"); + $animate.enter(options.backdrop, options.parent); + } + + /** + * Hide modal backdrop element... + */ + options.hideBackdrop = function hideBackdrop($destroy) { + if (options.backdrop) { + if ( !!$destroy ) options.backdrop.remove(); + else $animate.leave(options.backdrop); + } + + + if (options.disableParentScroll) { + options.restoreScroll(); + delete options.restoreScroll; + } + + options.hideBackdrop = null; + }; + } + + /** + * Inject ARIA-specific attributes appropriate for Dialogs + */ + function configureAria(element, options) { + + var role = (options.$type === 'alert') ? 'alertdialog' : 'dialog'; + var dialogContent = element.find('md-dialog-content'); + var existingDialogId = element.attr('id'); + var dialogContentId = 'dialogContent_' + (existingDialogId || $mdUtil.nextUid()); + + element.attr({ + 'role': role, + 'tabIndex': '-1' + }); + + if (dialogContent.length === 0) { + dialogContent = element; + // If the dialog element already had an ID, don't clobber it. + if (existingDialogId) { + dialogContentId = existingDialogId; + } + } + + dialogContent.attr('id', dialogContentId); + element.attr('aria-describedby', dialogContentId); + + if (options.ariaLabel) { + $mdAria.expect(element, 'aria-label', options.ariaLabel); + } + else { + $mdAria.expectAsync(element, 'aria-label', function() { + var words = dialogContent.text().split(/\s+/); + if (words.length > 3) words = words.slice(0, 3).concat('...'); + return words.join(' '); + }); + } + + // Set up elements before and after the dialog content to capture focus and + // redirect back into the dialog. + topFocusTrap = document.createElement('div'); + topFocusTrap.classList.add('md-dialog-focus-trap'); + topFocusTrap.tabIndex = 0; + + bottomFocusTrap = topFocusTrap.cloneNode(false); + + // When focus is about to move out of the dialog, we want to intercept it and redirect it + // back to the dialog element. + var focusHandler = function() { + element.focus(); + }; + topFocusTrap.addEventListener('focus', focusHandler); + bottomFocusTrap.addEventListener('focus', focusHandler); + + // The top focus trap inserted immeidately before the md-dialog element (as a sibling). + // The bottom focus trap is inserted at the very end of the md-dialog element (as a child). + element[0].parentNode.insertBefore(topFocusTrap, element[0]); + element.after(bottomFocusTrap); + } + + /** + * Prevents screen reader interaction behind modal window + * on swipe interfaces + */ + function lockScreenReader(element, options) { + var isHidden = true; + + // get raw DOM node + walkDOM(element[0]); + + options.unlockScreenReader = function() { + isHidden = false; + walkDOM(element[0]); + + options.unlockScreenReader = null; + }; + + /** + * Walk DOM to apply or remove aria-hidden on sibling nodes + * and parent sibling nodes + * + */ + function walkDOM(element) { + while (element.parentNode) { + if (element === document.body) { + return; + } + var children = element.parentNode.children; + for (var i = 0; i < children.length; i++) { + // skip over child if it is an ascendant of the dialog + // or a script or style tag + if (element !== children[i] && !isNodeOneOf(children[i], ['SCRIPT', 'STYLE'])) { + children[i].setAttribute('aria-hidden', isHidden); + } + } + + walkDOM(element = element.parentNode); + } + } + } + + /** + * Ensure the dialog container fill-stretches to the viewport + */ + function stretchDialogContainerToViewport(container, options) { + var isFixed = $window.getComputedStyle($document[0].body).position == 'fixed'; + var backdrop = options.backdrop ? $window.getComputedStyle(options.backdrop[0]) : null; + var height = backdrop ? Math.min($document[0].body.clientHeight, Math.ceil(Math.abs(parseInt(backdrop.height, 10)))) : 0; + + var previousStyles = { + top: container.css('top'), + height: container.css('height') + }; + + container.css({ + top: (isFixed ? $mdUtil.scrollTop(options.parent) : 0) + 'px', + height: height ? height + 'px' : '100%' + }); + + return function() { + // Reverts the modified styles back to the previous values. + // This is needed for contentElements, which should have the same styles after close + // as before. + container.css(previousStyles); + }; + } + + /** + * Dialog open and pop-in animation + */ + function dialogPopIn(container, options) { + // Add the `md-dialog-container` to the DOM + options.parent.append(container); + options.reverseContainerStretch = stretchDialogContainerToViewport(container, options); + + var dialogEl = container.find('md-dialog'); + var animator = $mdUtil.dom.animator; + var buildTranslateToOrigin = animator.calculateZoomToOrigin; + var translateOptions = {transitionInClass: 'md-transition-in', transitionOutClass: 'md-transition-out'}; + var from = animator.toTransformCss(buildTranslateToOrigin(dialogEl, options.openFrom || options.origin)); + var to = animator.toTransformCss(""); // defaults to center display (or parent or $rootElement) + + dialogEl.toggleClass('md-dialog-fullscreen', !!options.fullscreen); + + return animator + .translate3d(dialogEl, from, to, translateOptions) + .then(function(animateReversal) { + + // Build a reversal translate function synced to this translation... + options.reverseAnimate = function() { + delete options.reverseAnimate; + + if (options.closeTo) { + // Using the opposite classes to create a close animation to the closeTo element + translateOptions = {transitionInClass: 'md-transition-out', transitionOutClass: 'md-transition-in'}; + from = to; + to = animator.toTransformCss(buildTranslateToOrigin(dialogEl, options.closeTo)); + + return animator + .translate3d(dialogEl, from, to,translateOptions); + } + + return animateReversal( + to = animator.toTransformCss( + // in case the origin element has moved or is hidden, + // let's recalculate the translateCSS + buildTranslateToOrigin(dialogEl, options.origin) + ) + ); + + }; + + // Function to revert the generated animation styles on the dialog element. + // Useful when using a contentElement instead of a template. + options.clearAnimate = function() { + delete options.clearAnimate; + + // Remove the transition classes, added from $animateCSS, since those can't be removed + // by reversely running the animator. + dialogEl.removeClass([ + translateOptions.transitionOutClass, + translateOptions.transitionInClass + ].join(' ')); + + // Run the animation reversely to remove the previous added animation styles. + return animator.translate3d(dialogEl, to, animator.toTransformCss(''), {}); + }; + + return true; + }); + } + + /** + * Dialog close and pop-out animation + */ + function dialogPopOut(container, options) { + return options.reverseAnimate().then(function() { + if (options.contentElement) { + // When we use a contentElement, we want the element to be the same as before. + // That means, that we have to clear all the animation properties, like transform. + options.clearAnimate(); + } + }); + } + + /** + * Utility function to filter out raw DOM nodes + */ + function isNodeOneOf(elem, nodeTypeArray) { + if (nodeTypeArray.indexOf(elem.nodeName) !== -1) { + return true; + } + } + + } +} + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.components.divider + * @description Divider module! + */ +MdDividerDirective.$inject = ["$mdTheming"]; +angular.module('material.components.divider', [ + 'material.core' +]) + .directive('mdDivider', MdDividerDirective); + +/** + * @ngdoc directive + * @name mdDivider + * @module material.components.divider + * @restrict E + * + * @description + * Dividers group and separate content within lists and page layouts using strong visual and spatial distinctions. This divider is a thin rule, lightweight enough to not distract the user from content. + * + * @param {boolean=} md-inset Add this attribute to activate the inset divider style. + * @usage + * + * + * + * + * + * + */ +function MdDividerDirective($mdTheming) { + return { + restrict: 'E', + link: $mdTheming + }; +} + +})(); +(function(){ +"use strict"; + +(function() { + 'use strict'; + + /** + * @ngdoc module + * @name material.components.fabActions + */ + MdFabActionsDirective.$inject = ["$mdUtil"]; + angular + .module('material.components.fabActions', ['material.core']) + .directive('mdFabActions', MdFabActionsDirective); + + /** + * @ngdoc directive + * @name mdFabActions + * @module material.components.fabActions + * + * @restrict E + * + * @description + * The `` directive is used inside of a `` or + * `` directive to mark an element (or elements) as the actions and setup the + * proper event listeners. + * + * @usage + * See the `` or `` directives for example usage. + */ + function MdFabActionsDirective($mdUtil) { + return { + restrict: 'E', + + require: ['^?mdFabSpeedDial', '^?mdFabToolbar'], + + compile: function(element, attributes) { + var children = element.children(); + + var hasNgRepeat = $mdUtil.prefixer().hasAttribute(children, 'ng-repeat'); + + // Support both ng-repeat and static content + if (hasNgRepeat) { + children.addClass('md-fab-action-item'); + } else { + // Wrap every child in a new div and add a class that we can scale/fling independently + children.wrap('
      '); + } + } + } + } + +})(); + +})(); +(function(){ +"use strict"; + +(function() { + 'use strict'; + + MdFabController.$inject = ["$scope", "$element", "$animate", "$mdUtil", "$mdConstant", "$timeout"]; + angular.module('material.components.fabShared', ['material.core']) + .controller('MdFabController', MdFabController); + + function MdFabController($scope, $element, $animate, $mdUtil, $mdConstant, $timeout) { + var vm = this; + + // NOTE: We use async eval(s) below to avoid conflicts with any existing digest loops + + vm.open = function() { + $scope.$evalAsync("vm.isOpen = true"); + }; + + vm.close = function() { + // Async eval to avoid conflicts with existing digest loops + $scope.$evalAsync("vm.isOpen = false"); + + // Focus the trigger when the element closes so users can still tab to the next item + $element.find('md-fab-trigger')[0].focus(); + }; + + // Toggle the open/close state when the trigger is clicked + vm.toggle = function() { + $scope.$evalAsync("vm.isOpen = !vm.isOpen"); + }; + + setupDefaults(); + setupListeners(); + setupWatchers(); + + var initialAnimationAttempts = 0; + fireInitialAnimations(); + + function setupDefaults() { + // Set the default direction to 'down' if none is specified + vm.direction = vm.direction || 'down'; + + // Set the default to be closed + vm.isOpen = vm.isOpen || false; + + // Start the keyboard interaction at the first action + resetActionIndex(); + + // Add an animations waiting class so we know not to run + $element.addClass('md-animations-waiting'); + } + + function setupListeners() { + var eventTypes = [ + 'click', 'focusin', 'focusout' + ]; + + // Add our listeners + angular.forEach(eventTypes, function(eventType) { + $element.on(eventType, parseEvents); + }); + + // Remove our listeners when destroyed + $scope.$on('$destroy', function() { + angular.forEach(eventTypes, function(eventType) { + $element.off(eventType, parseEvents); + }); + + // remove any attached keyboard handlers in case element is removed while + // speed dial is open + disableKeyboard(); + }); + } + + var closeTimeout; + function parseEvents(event) { + // If the event is a click, just handle it + if (event.type == 'click') { + handleItemClick(event); + } + + // If we focusout, set a timeout to close the element + if (event.type == 'focusout' && !closeTimeout) { + closeTimeout = $timeout(function() { + vm.close(); + }, 100, false); + } + + // If we see a focusin and there is a timeout about to run, cancel it so we stay open + if (event.type == 'focusin' && closeTimeout) { + $timeout.cancel(closeTimeout); + closeTimeout = null; + } + } + + function resetActionIndex() { + vm.currentActionIndex = -1; + } + + function setupWatchers() { + // Watch for changes to the direction and update classes/attributes + $scope.$watch('vm.direction', function(newDir, oldDir) { + // Add the appropriate classes so we can target the direction in the CSS + $animate.removeClass($element, 'md-' + oldDir); + $animate.addClass($element, 'md-' + newDir); + + // Reset the action index since it may have changed + resetActionIndex(); + }); + + var trigger, actions; + + // Watch for changes to md-open + $scope.$watch('vm.isOpen', function(isOpen) { + // Reset the action index since it may have changed + resetActionIndex(); + + // We can't get the trigger/actions outside of the watch because the component hasn't been + // linked yet, so we wait until the first watch fires to cache them. + if (!trigger || !actions) { + trigger = getTriggerElement(); + actions = getActionsElement(); + } + + if (isOpen) { + enableKeyboard(); + } else { + disableKeyboard(); + } + + var toAdd = isOpen ? 'md-is-open' : ''; + var toRemove = isOpen ? '' : 'md-is-open'; + + // Set the proper ARIA attributes + trigger.attr('aria-haspopup', true); + trigger.attr('aria-expanded', isOpen); + actions.attr('aria-hidden', !isOpen); + + // Animate the CSS classes + $animate.setClass($element, toAdd, toRemove); + }); + } + + function fireInitialAnimations() { + // If the element is actually visible on the screen + if ($element[0].scrollHeight > 0) { + // Fire our animation + $animate.addClass($element, '_md-animations-ready').then(function() { + // Remove the waiting class + $element.removeClass('md-animations-waiting'); + }); + } + + // Otherwise, try for up to 1 second before giving up + else if (initialAnimationAttempts < 10) { + $timeout(fireInitialAnimations, 100); + + // Increment our counter + initialAnimationAttempts = initialAnimationAttempts + 1; + } + } + + function enableKeyboard() { + $element.on('keydown', keyPressed); + + // On the next tick, setup a check for outside clicks; we do this on the next tick to avoid + // clicks/touches that result in the isOpen attribute changing (e.g. a bound radio button) + $mdUtil.nextTick(function() { + angular.element(document).on('click touchend', checkForOutsideClick); + }); + + // TODO: On desktop, we should be able to reset the indexes so you cannot tab through, but + // this breaks accessibility, especially on mobile, since you have no arrow keys to press + //resetActionTabIndexes(); + } + + function disableKeyboard() { + $element.off('keydown', keyPressed); + angular.element(document).off('click touchend', checkForOutsideClick); + } + + function checkForOutsideClick(event) { + if (event.target) { + var closestTrigger = $mdUtil.getClosest(event.target, 'md-fab-trigger'); + var closestActions = $mdUtil.getClosest(event.target, 'md-fab-actions'); + + if (!closestTrigger && !closestActions) { + vm.close(); + } + } + } + + function keyPressed(event) { + switch (event.which) { + case $mdConstant.KEY_CODE.ESCAPE: vm.close(); event.preventDefault(); return false; + case $mdConstant.KEY_CODE.LEFT_ARROW: doKeyLeft(event); return false; + case $mdConstant.KEY_CODE.UP_ARROW: doKeyUp(event); return false; + case $mdConstant.KEY_CODE.RIGHT_ARROW: doKeyRight(event); return false; + case $mdConstant.KEY_CODE.DOWN_ARROW: doKeyDown(event); return false; + } + } + + function doActionPrev(event) { + focusAction(event, -1); + } + + function doActionNext(event) { + focusAction(event, 1); + } + + function focusAction(event, direction) { + var actions = resetActionTabIndexes(); + + // Increment/decrement the counter with restrictions + vm.currentActionIndex = vm.currentActionIndex + direction; + vm.currentActionIndex = Math.min(actions.length - 1, vm.currentActionIndex); + vm.currentActionIndex = Math.max(0, vm.currentActionIndex); + + // Focus the element + var focusElement = angular.element(actions[vm.currentActionIndex]).children()[0]; + angular.element(focusElement).attr('tabindex', 0); + focusElement.focus(); + + // Make sure the event doesn't bubble and cause something else + event.preventDefault(); + event.stopImmediatePropagation(); + } + + function resetActionTabIndexes() { + // Grab all of the actions + var actions = getActionsElement()[0].querySelectorAll('.md-fab-action-item'); + + // Disable all other actions for tabbing + angular.forEach(actions, function(action) { + angular.element(angular.element(action).children()[0]).attr('tabindex', -1); + }); + + return actions; + } + + function doKeyLeft(event) { + if (vm.direction === 'left') { + doActionNext(event); + } else { + doActionPrev(event); + } + } + + function doKeyUp(event) { + if (vm.direction === 'down') { + doActionPrev(event); + } else { + doActionNext(event); + } + } + + function doKeyRight(event) { + if (vm.direction === 'left') { + doActionPrev(event); + } else { + doActionNext(event); + } + } + + function doKeyDown(event) { + if (vm.direction === 'up') { + doActionPrev(event); + } else { + doActionNext(event); + } + } + + function isTrigger(element) { + return $mdUtil.getClosest(element, 'md-fab-trigger'); + } + + function isAction(element) { + return $mdUtil.getClosest(element, 'md-fab-actions'); + } + + function handleItemClick(event) { + if (isTrigger(event.target)) { + vm.toggle(); + } + + if (isAction(event.target)) { + vm.close(); + } + } + + function getTriggerElement() { + return $element.find('md-fab-trigger'); + } + + function getActionsElement() { + return $element.find('md-fab-actions'); + } + } +})(); + +})(); +(function(){ +"use strict"; + +(function() { + 'use strict'; + + /** + * The duration of the CSS animation in milliseconds. + * + * @type {number} + */ + MdFabSpeedDialFlingAnimation.$inject = ["$timeout"]; + MdFabSpeedDialScaleAnimation.$inject = ["$timeout"]; + var cssAnimationDuration = 300; + + /** + * @ngdoc module + * @name material.components.fabSpeedDial + */ + angular + // Declare our module + .module('material.components.fabSpeedDial', [ + 'material.core', + 'material.components.fabShared', + 'material.components.fabActions' + ]) + + // Register our directive + .directive('mdFabSpeedDial', MdFabSpeedDialDirective) + + // Register our custom animations + .animation('.md-fling', MdFabSpeedDialFlingAnimation) + .animation('.md-scale', MdFabSpeedDialScaleAnimation) + + // Register a service for each animation so that we can easily inject them into unit tests + .service('mdFabSpeedDialFlingAnimation', MdFabSpeedDialFlingAnimation) + .service('mdFabSpeedDialScaleAnimation', MdFabSpeedDialScaleAnimation); + + /** + * @ngdoc directive + * @name mdFabSpeedDial + * @module material.components.fabSpeedDial + * + * @restrict E + * + * @description + * The `` directive is used to present a series of popup elements (usually + * ``s) for quick access to common actions. + * + * There are currently two animations available by applying one of the following classes to + * the component: + * + * - `md-fling` - The speed dial items appear from underneath the trigger and move into their + * appropriate positions. + * - `md-scale` - The speed dial items appear in their proper places by scaling from 0% to 100%. + * + * You may also easily position the trigger by applying one one of the following classes to the + * `` element: + * - `md-fab-top-left` + * - `md-fab-top-right` + * - `md-fab-bottom-left` + * - `md-fab-bottom-right` + * + * These CSS classes use `position: absolute`, so you need to ensure that the container element + * also uses `position: absolute` or `position: relative` in order for them to work. + * + * Additionally, you may use the standard `ng-mouseenter` and `ng-mouseleave` directives to + * open or close the speed dial. However, if you wish to allow users to hover over the empty + * space where the actions will appear, you must also add the `md-hover-full` class to the speed + * dial element. Without this, the hover effect will only occur on top of the trigger. + * + * See the demos for more information. + * + * ## Troubleshooting + * + * If your speed dial shows the closing animation upon launch, you may need to use `ng-cloak` on + * the parent container to ensure that it is only visible once ready. We have plans to remove this + * necessity in the future. + * + * @usage + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * @param {string} md-direction From which direction you would like the speed dial to appear + * relative to the trigger element. + * @param {expression=} md-open Programmatically control whether or not the speed-dial is visible. + */ + function MdFabSpeedDialDirective() { + return { + restrict: 'E', + + scope: { + direction: '@?mdDirection', + isOpen: '=?mdOpen' + }, + + bindToController: true, + controller: 'MdFabController', + controllerAs: 'vm', + + link: FabSpeedDialLink + }; + + function FabSpeedDialLink(scope, element) { + // Prepend an element to hold our CSS variables so we can use them in the animations below + element.prepend('
      '); + } + } + + function MdFabSpeedDialFlingAnimation($timeout) { + function delayDone(done) { $timeout(done, cssAnimationDuration, false); } + + function runAnimation(element) { + // Don't run if we are still waiting and we are not ready + if (element.hasClass('md-animations-waiting') && !element.hasClass('_md-animations-ready')) { + return; + } + + var el = element[0]; + var ctrl = element.controller('mdFabSpeedDial'); + var items = el.querySelectorAll('.md-fab-action-item'); + + // Grab our trigger element + var triggerElement = el.querySelector('md-fab-trigger'); + + // Grab our element which stores CSS variables + var variablesElement = el.querySelector('._md-css-variables'); + + // Setup JS variables based on our CSS variables + var startZIndex = parseInt(window.getComputedStyle(variablesElement).zIndex); + + // Always reset the items to their natural position/state + angular.forEach(items, function(item, index) { + var styles = item.style; + + styles.transform = styles.webkitTransform = ''; + styles.transitionDelay = ''; + styles.opacity = 1; + + // Make the items closest to the trigger have the highest z-index + styles.zIndex = (items.length - index) + startZIndex; + }); + + // Set the trigger to be above all of the actions so they disappear behind it. + triggerElement.style.zIndex = startZIndex + items.length + 1; + + // If the control is closed, hide the items behind the trigger + if (!ctrl.isOpen) { + angular.forEach(items, function(item, index) { + var newPosition, axis; + var styles = item.style; + + // Make sure to account for differences in the dimensions of the trigger verses the items + // so that we can properly center everything; this helps hide the item's shadows behind + // the trigger. + var triggerItemHeightOffset = (triggerElement.clientHeight - item.clientHeight) / 2; + var triggerItemWidthOffset = (triggerElement.clientWidth - item.clientWidth) / 2; + + switch (ctrl.direction) { + case 'up': + newPosition = (item.scrollHeight * (index + 1) + triggerItemHeightOffset); + axis = 'Y'; + break; + case 'down': + newPosition = -(item.scrollHeight * (index + 1) + triggerItemHeightOffset); + axis = 'Y'; + break; + case 'left': + newPosition = (item.scrollWidth * (index + 1) + triggerItemWidthOffset); + axis = 'X'; + break; + case 'right': + newPosition = -(item.scrollWidth * (index + 1) + triggerItemWidthOffset); + axis = 'X'; + break; + } + + var newTranslate = 'translate' + axis + '(' + newPosition + 'px)'; + + styles.transform = styles.webkitTransform = newTranslate; + }); + } + } + + return { + addClass: function(element, className, done) { + if (element.hasClass('md-fling')) { + runAnimation(element); + delayDone(done); + } else { + done(); + } + }, + removeClass: function(element, className, done) { + runAnimation(element); + delayDone(done); + } + } + } + + function MdFabSpeedDialScaleAnimation($timeout) { + function delayDone(done) { $timeout(done, cssAnimationDuration, false); } + + var delay = 65; + + function runAnimation(element) { + var el = element[0]; + var ctrl = element.controller('mdFabSpeedDial'); + var items = el.querySelectorAll('.md-fab-action-item'); + + // Grab our element which stores CSS variables + var variablesElement = el.querySelector('._md-css-variables'); + + // Setup JS variables based on our CSS variables + var startZIndex = parseInt(window.getComputedStyle(variablesElement).zIndex); + + // Always reset the items to their natural position/state + angular.forEach(items, function(item, index) { + var styles = item.style, + offsetDelay = index * delay; + + styles.opacity = ctrl.isOpen ? 1 : 0; + styles.transform = styles.webkitTransform = ctrl.isOpen ? 'scale(1)' : 'scale(0)'; + styles.transitionDelay = (ctrl.isOpen ? offsetDelay : (items.length - offsetDelay)) + 'ms'; + + // Make the items closest to the trigger have the highest z-index + styles.zIndex = (items.length - index) + startZIndex; + }); + } + + return { + addClass: function(element, className, done) { + runAnimation(element); + delayDone(done); + }, + + removeClass: function(element, className, done) { + runAnimation(element); + delayDone(done); + } + } + } +})(); + +})(); +(function(){ +"use strict"; + +(function() { + 'use strict'; + + /** + * @ngdoc module + * @name material.components.fabToolbar + */ + angular + // Declare our module + .module('material.components.fabToolbar', [ + 'material.core', + 'material.components.fabShared', + 'material.components.fabActions' + ]) + + // Register our directive + .directive('mdFabToolbar', MdFabToolbarDirective) + + // Register our custom animations + .animation('.md-fab-toolbar', MdFabToolbarAnimation) + + // Register a service for the animation so that we can easily inject it into unit tests + .service('mdFabToolbarAnimation', MdFabToolbarAnimation); + + /** + * @ngdoc directive + * @name mdFabToolbar + * @module material.components.fabToolbar + * + * @restrict E + * + * @description + * + * The `` directive is used present a toolbar of elements (usually ``s) + * for quick access to common actions when a floating action button is activated (via click or + * keyboard navigation). + * + * You may also easily position the trigger by applying one one of the following classes to the + * `` element: + * - `md-fab-top-left` + * - `md-fab-top-right` + * - `md-fab-bottom-left` + * - `md-fab-bottom-right` + * + * These CSS classes use `position: absolute`, so you need to ensure that the container element + * also uses `position: absolute` or `position: relative` in order for them to work. + * + * @usage + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * @param {string} md-direction From which direction you would like the toolbar items to appear + * relative to the trigger element. Supports `left` and `right` directions. + * @param {expression=} md-open Programmatically control whether or not the toolbar is visible. + */ + function MdFabToolbarDirective() { + return { + restrict: 'E', + transclude: true, + template: '
      ' + + '
      ' + + '
      ', + + scope: { + direction: '@?mdDirection', + isOpen: '=?mdOpen' + }, + + bindToController: true, + controller: 'MdFabController', + controllerAs: 'vm', + + link: link + }; + + function link(scope, element, attributes) { + // Add the base class for animations + element.addClass('md-fab-toolbar'); + + // Prepend the background element to the trigger's button + element.find('md-fab-trigger').find('button') + .prepend('
      '); + } + } + + function MdFabToolbarAnimation() { + + function runAnimation(element, className, done) { + // If no className was specified, don't do anything + if (!className) { + return; + } + + var el = element[0]; + var ctrl = element.controller('mdFabToolbar'); + + // Grab the relevant child elements + var backgroundElement = el.querySelector('.md-fab-toolbar-background'); + var triggerElement = el.querySelector('md-fab-trigger button'); + var toolbarElement = el.querySelector('md-toolbar'); + var iconElement = el.querySelector('md-fab-trigger button md-icon'); + var actions = element.find('md-fab-actions').children(); + + // If we have both elements, use them to position the new background + if (triggerElement && backgroundElement) { + // Get our variables + var color = window.getComputedStyle(triggerElement).getPropertyValue('background-color'); + var width = el.offsetWidth; + var height = el.offsetHeight; + + // Make it twice as big as it should be since we scale from the center + var scale = 2 * (width / triggerElement.offsetWidth); + + // Set some basic styles no matter what animation we're doing + backgroundElement.style.backgroundColor = color; + backgroundElement.style.borderRadius = width + 'px'; + + // If we're open + if (ctrl.isOpen) { + // Turn on toolbar pointer events when closed + toolbarElement.style.pointerEvents = 'inherit'; + + backgroundElement.style.width = triggerElement.offsetWidth + 'px'; + backgroundElement.style.height = triggerElement.offsetHeight + 'px'; + backgroundElement.style.transform = 'scale(' + scale + ')'; + + // Set the next close animation to have the proper delays + backgroundElement.style.transitionDelay = '0ms'; + iconElement && (iconElement.style.transitionDelay = '.3s'); + + // Apply a transition delay to actions + angular.forEach(actions, function(action, index) { + action.style.transitionDelay = (actions.length - index) * 25 + 'ms'; + }); + } else { + // Turn off toolbar pointer events when closed + toolbarElement.style.pointerEvents = 'none'; + + // Scale it back down to the trigger's size + backgroundElement.style.transform = 'scale(1)'; + + // Reset the position + backgroundElement.style.top = '0'; + + if (element.hasClass('md-right')) { + backgroundElement.style.left = '0'; + backgroundElement.style.right = null; + } + + if (element.hasClass('md-left')) { + backgroundElement.style.right = '0'; + backgroundElement.style.left = null; + } + + // Set the next open animation to have the proper delays + backgroundElement.style.transitionDelay = '200ms'; + iconElement && (iconElement.style.transitionDelay = '0ms'); + + // Apply a transition delay to actions + angular.forEach(actions, function(action, index) { + action.style.transitionDelay = 200 + (index * 25) + 'ms'; + }); + } + } + } + + return { + addClass: function(element, className, done) { + runAnimation(element, className, done); + done(); + }, + + removeClass: function(element, className, done) { + runAnimation(element, className, done); + done(); + } + } + } +})(); + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.components.gridList + */ +GridListController.$inject = ["$mdUtil"]; +GridLayoutFactory.$inject = ["$mdUtil"]; +GridListDirective.$inject = ["$interpolate", "$mdConstant", "$mdGridLayout", "$mdMedia"]; +GridTileDirective.$inject = ["$mdMedia"]; +angular.module('material.components.gridList', ['material.core']) + .directive('mdGridList', GridListDirective) + .directive('mdGridTile', GridTileDirective) + .directive('mdGridTileFooter', GridTileCaptionDirective) + .directive('mdGridTileHeader', GridTileCaptionDirective) + .factory('$mdGridLayout', GridLayoutFactory); + +/** + * @ngdoc directive + * @name mdGridList + * @module material.components.gridList + * @restrict E + * @description + * Grid lists are an alternative to standard list views. Grid lists are distinct + * from grids used for layouts and other visual presentations. + * + * A grid list is best suited to presenting a homogenous data type, typically + * images, and is optimized for visual comprehension and differentiating between + * like data types. + * + * A grid list is a continuous element consisting of tessellated, regular + * subdivisions called cells that contain tiles (`md-grid-tile`). + * + * Concept of grid explained visually + * Grid concepts legend + * + * Cells are arrayed vertically and horizontally within the grid. + * + * Tiles hold content and can span one or more cells vertically or horizontally. + * + * ### Responsive Attributes + * + * The `md-grid-list` directive supports "responsive" attributes, which allow + * different `md-cols`, `md-gutter` and `md-row-height` values depending on the + * currently matching media query. + * + * In order to set a responsive attribute, first define the fallback value with + * the standard attribute name, then add additional attributes with the + * following convention: `{base-attribute-name}-{media-query-name}="{value}"` + * (ie. `md-cols-lg="8"`) + * + * @param {number} md-cols Number of columns in the grid. + * @param {string} md-row-height One of + *
        + *
      • CSS length - Fixed height rows (eg. `8px` or `1rem`)
      • + *
      • `{width}:{height}` - Ratio of width to height (eg. + * `md-row-height="16:9"`)
      • + *
      • `"fit"` - Height will be determined by subdividing the available + * height by the number of rows
      • + *
      + * @param {string=} md-gutter The amount of space between tiles in CSS units + * (default 1px) + * @param {expression=} md-on-layout Expression to evaluate after layout. Event + * object is available as `$event`, and contains performance information. + * + * @usage + * Basic: + * + * + * + * + * + * + * Fixed-height rows: + * + * + * + * + * + * + * Fit rows: + * + * + * + * + * + * + * Using responsive attributes: + * + * + * + * + * + */ +function GridListDirective($interpolate, $mdConstant, $mdGridLayout, $mdMedia) { + return { + restrict: 'E', + controller: GridListController, + scope: { + mdOnLayout: '&' + }, + link: postLink + }; + + function postLink(scope, element, attrs, ctrl) { + element.addClass('_md'); // private md component indicator for styling + + // Apply semantics + element.attr('role', 'list'); + + // Provide the controller with a way to trigger layouts. + ctrl.layoutDelegate = layoutDelegate; + + var invalidateLayout = angular.bind(ctrl, ctrl.invalidateLayout), + unwatchAttrs = watchMedia(); + scope.$on('$destroy', unwatchMedia); + + /** + * Watches for changes in media, invalidating layout as necessary. + */ + function watchMedia() { + for (var mediaName in $mdConstant.MEDIA) { + $mdMedia(mediaName); // initialize + $mdMedia.getQuery($mdConstant.MEDIA[mediaName]) + .addListener(invalidateLayout); + } + return $mdMedia.watchResponsiveAttributes( + ['md-cols', 'md-row-height', 'md-gutter'], attrs, layoutIfMediaMatch); + } + + function unwatchMedia() { + ctrl.layoutDelegate = angular.noop; + + unwatchAttrs(); + for (var mediaName in $mdConstant.MEDIA) { + $mdMedia.getQuery($mdConstant.MEDIA[mediaName]) + .removeListener(invalidateLayout); + } + } + + /** + * Performs grid layout if the provided mediaName matches the currently + * active media type. + */ + function layoutIfMediaMatch(mediaName) { + if (mediaName == null) { + // TODO(shyndman): It would be nice to only layout if we have + // instances of attributes using this media type + ctrl.invalidateLayout(); + } else if ($mdMedia(mediaName)) { + ctrl.invalidateLayout(); + } + } + + var lastLayoutProps; + + /** + * Invokes the layout engine, and uses its results to lay out our + * tile elements. + * + * @param {boolean} tilesInvalidated Whether tiles have been + * added/removed/moved since the last layout. This is to avoid situations + * where tiles are replaced with properties identical to their removed + * counterparts. + */ + function layoutDelegate(tilesInvalidated) { + var tiles = getTileElements(); + var props = { + tileSpans: getTileSpans(tiles), + colCount: getColumnCount(), + rowMode: getRowMode(), + rowHeight: getRowHeight(), + gutter: getGutter() + }; + + if (!tilesInvalidated && angular.equals(props, lastLayoutProps)) { + return; + } + + var performance = + $mdGridLayout(props.colCount, props.tileSpans, tiles) + .map(function(tilePositions, rowCount) { + return { + grid: { + element: element, + style: getGridStyle(props.colCount, rowCount, + props.gutter, props.rowMode, props.rowHeight) + }, + tiles: tilePositions.map(function(ps, i) { + return { + element: angular.element(tiles[i]), + style: getTileStyle(ps.position, ps.spans, + props.colCount, rowCount, + props.gutter, props.rowMode, props.rowHeight) + } + }) + } + }) + .reflow() + .performance(); + + // Report layout + scope.mdOnLayout({ + $event: { + performance: performance + } + }); + + lastLayoutProps = props; + } + + // Use $interpolate to do some simple string interpolation as a convenience. + + var startSymbol = $interpolate.startSymbol(); + var endSymbol = $interpolate.endSymbol(); + + // Returns an expression wrapped in the interpolator's start and end symbols. + function expr(exprStr) { + return startSymbol + exprStr + endSymbol; + } + + // The amount of space a single 1x1 tile would take up (either width or height), used as + // a basis for other calculations. This consists of taking the base size percent (as would be + // if evenly dividing the size between cells), and then subtracting the size of one gutter. + // However, since there are no gutters on the edges, each tile only uses a fration + // (gutterShare = numGutters / numCells) of the gutter size. (Imagine having one gutter per + // tile, and then breaking up the extra gutter on the edge evenly among the cells). + var UNIT = $interpolate(expr('share') + '% - (' + expr('gutter') + ' * ' + expr('gutterShare') + ')'); + + // The horizontal or vertical position of a tile, e.g., the 'top' or 'left' property value. + // The position comes the size of a 1x1 tile plus gutter for each previous tile in the + // row/column (offset). + var POSITION = $interpolate('calc((' + expr('unit') + ' + ' + expr('gutter') + ') * ' + expr('offset') + ')'); + + // The actual size of a tile, e.g., width or height, taking rowSpan or colSpan into account. + // This is computed by multiplying the base unit by the rowSpan/colSpan, and then adding back + // in the space that the gutter would normally have used (which was already accounted for in + // the base unit calculation). + var DIMENSION = $interpolate('calc((' + expr('unit') + ') * ' + expr('span') + ' + (' + expr('span') + ' - 1) * ' + expr('gutter') + ')'); + + /** + * Gets the styles applied to a tile element described by the given parameters. + * @param {{row: number, col: number}} position The row and column indices of the tile. + * @param {{row: number, col: number}} spans The rowSpan and colSpan of the tile. + * @param {number} colCount The number of columns. + * @param {number} rowCount The number of rows. + * @param {string} gutter The amount of space between tiles. This will be something like + * '5px' or '2em'. + * @param {string} rowMode The row height mode. Can be one of: + * 'fixed': all rows have a fixed size, given by rowHeight, + * 'ratio': row height defined as a ratio to width, or + * 'fit': fit to the grid-list element height, divinding evenly among rows. + * @param {string|number} rowHeight The height of a row. This is only used for 'fixed' mode and + * for 'ratio' mode. For 'ratio' mode, this is the *ratio* of width-to-height (e.g., 0.75). + * @returns {Object} Map of CSS properties to be applied to the style element. Will define + * values for top, left, width, height, marginTop, and paddingTop. + */ + function getTileStyle(position, spans, colCount, rowCount, gutter, rowMode, rowHeight) { + // TODO(shyndman): There are style caching opportunities here. + + // Percent of the available horizontal space that one column takes up. + var hShare = (1 / colCount) * 100; + + // Fraction of the gutter size that each column takes up. + var hGutterShare = (colCount - 1) / colCount; + + // Base horizontal size of a column. + var hUnit = UNIT({share: hShare, gutterShare: hGutterShare, gutter: gutter}); + + // The width and horizontal position of each tile is always calculated the same way, but the + // height and vertical position depends on the rowMode. + var style = { + left: POSITION({ unit: hUnit, offset: position.col, gutter: gutter }), + width: DIMENSION({ unit: hUnit, span: spans.col, gutter: gutter }), + // resets + paddingTop: '', + marginTop: '', + top: '', + height: '' + }; + + switch (rowMode) { + case 'fixed': + // In fixed mode, simply use the given rowHeight. + style.top = POSITION({ unit: rowHeight, offset: position.row, gutter: gutter }); + style.height = DIMENSION({ unit: rowHeight, span: spans.row, gutter: gutter }); + break; + + case 'ratio': + // Percent of the available vertical space that one row takes up. Here, rowHeight holds + // the ratio value. For example, if the width:height ratio is 4:3, rowHeight = 1.333. + var vShare = hShare / rowHeight; + + // Base veritcal size of a row. + var vUnit = UNIT({ share: vShare, gutterShare: hGutterShare, gutter: gutter }); + + // padidngTop and marginTop are used to maintain the given aspect ratio, as + // a percentage-based value for these properties is applied to the *width* of the + // containing block. See http://www.w3.org/TR/CSS2/box.html#margin-properties + style.paddingTop = DIMENSION({ unit: vUnit, span: spans.row, gutter: gutter}); + style.marginTop = POSITION({ unit: vUnit, offset: position.row, gutter: gutter }); + break; + + case 'fit': + // Fraction of the gutter size that each column takes up. + var vGutterShare = (rowCount - 1) / rowCount; + + // Percent of the available vertical space that one row takes up. + var vShare = (1 / rowCount) * 100; + + // Base vertical size of a row. + var vUnit = UNIT({share: vShare, gutterShare: vGutterShare, gutter: gutter}); + + style.top = POSITION({unit: vUnit, offset: position.row, gutter: gutter}); + style.height = DIMENSION({unit: vUnit, span: spans.row, gutter: gutter}); + break; + } + + return style; + } + + function getGridStyle(colCount, rowCount, gutter, rowMode, rowHeight) { + var style = {}; + + switch(rowMode) { + case 'fixed': + style.height = DIMENSION({ unit: rowHeight, span: rowCount, gutter: gutter }); + style.paddingBottom = ''; + break; + + case 'ratio': + // rowHeight is width / height + var hGutterShare = colCount === 1 ? 0 : (colCount - 1) / colCount, + hShare = (1 / colCount) * 100, + vShare = hShare * (1 / rowHeight), + vUnit = UNIT({ share: vShare, gutterShare: hGutterShare, gutter: gutter }); + + style.height = ''; + style.paddingBottom = DIMENSION({ unit: vUnit, span: rowCount, gutter: gutter}); + break; + + case 'fit': + // noop, as the height is user set + break; + } + + return style; + } + + function getTileElements() { + return [].filter.call(element.children(), function(ele) { + return ele.tagName == 'MD-GRID-TILE' && !ele.$$mdDestroyed; + }); + } + + /** + * Gets an array of objects containing the rowspan and colspan for each tile. + * @returns {Array<{row: number, col: number}>} + */ + function getTileSpans(tileElements) { + return [].map.call(tileElements, function(ele) { + var ctrl = angular.element(ele).controller('mdGridTile'); + return { + row: parseInt( + $mdMedia.getResponsiveAttribute(ctrl.$attrs, 'md-rowspan'), 10) || 1, + col: parseInt( + $mdMedia.getResponsiveAttribute(ctrl.$attrs, 'md-colspan'), 10) || 1 + }; + }); + } + + function getColumnCount() { + var colCount = parseInt($mdMedia.getResponsiveAttribute(attrs, 'md-cols'), 10); + if (isNaN(colCount)) { + throw 'md-grid-list: md-cols attribute was not found, or contained a non-numeric value'; + } + return colCount; + } + + function getGutter() { + return applyDefaultUnit($mdMedia.getResponsiveAttribute(attrs, 'md-gutter') || 1); + } + + function getRowHeight() { + var rowHeight = $mdMedia.getResponsiveAttribute(attrs, 'md-row-height'); + if (!rowHeight) { + throw 'md-grid-list: md-row-height attribute was not found'; + } + + switch (getRowMode()) { + case 'fixed': + return applyDefaultUnit(rowHeight); + case 'ratio': + var whRatio = rowHeight.split(':'); + return parseFloat(whRatio[0]) / parseFloat(whRatio[1]); + case 'fit': + return 0; // N/A + } + } + + function getRowMode() { + var rowHeight = $mdMedia.getResponsiveAttribute(attrs, 'md-row-height'); + if (!rowHeight) { + throw 'md-grid-list: md-row-height attribute was not found'; + } + + if (rowHeight == 'fit') { + return 'fit'; + } else if (rowHeight.indexOf(':') !== -1) { + return 'ratio'; + } else { + return 'fixed'; + } + } + + function applyDefaultUnit(val) { + return /\D$/.test(val) ? val : val + 'px'; + } + } +} + +/* @ngInject */ +function GridListController($mdUtil) { + this.layoutInvalidated = false; + this.tilesInvalidated = false; + this.$timeout_ = $mdUtil.nextTick; + this.layoutDelegate = angular.noop; +} + +GridListController.prototype = { + invalidateTiles: function() { + this.tilesInvalidated = true; + this.invalidateLayout(); + }, + + invalidateLayout: function() { + if (this.layoutInvalidated) { + return; + } + this.layoutInvalidated = true; + this.$timeout_(angular.bind(this, this.layout)); + }, + + layout: function() { + try { + this.layoutDelegate(this.tilesInvalidated); + } finally { + this.layoutInvalidated = false; + this.tilesInvalidated = false; + } + } +}; + + +/* @ngInject */ +function GridLayoutFactory($mdUtil) { + var defaultAnimator = GridTileAnimator; + + /** + * Set the reflow animator callback + */ + GridLayout.animateWith = function(customAnimator) { + defaultAnimator = !angular.isFunction(customAnimator) ? GridTileAnimator : customAnimator; + }; + + return GridLayout; + + /** + * Publish layout function + */ + function GridLayout(colCount, tileSpans) { + var self, layoutInfo, gridStyles, layoutTime, mapTime, reflowTime; + + layoutTime = $mdUtil.time(function() { + layoutInfo = calculateGridFor(colCount, tileSpans); + }); + + return self = { + + /** + * An array of objects describing each tile's position in the grid. + */ + layoutInfo: function() { + return layoutInfo; + }, + + /** + * Maps grid positioning to an element and a set of styles using the + * provided updateFn. + */ + map: function(updateFn) { + mapTime = $mdUtil.time(function() { + var info = self.layoutInfo(); + gridStyles = updateFn(info.positioning, info.rowCount); + }); + return self; + }, + + /** + * Default animator simply sets the element.css( ). An alternate + * animator can be provided as an argument. The function has the following + * signature: + * + * function({grid: {element: JQLite, style: Object}, tiles: Array<{element: JQLite, style: Object}>) + */ + reflow: function(animatorFn) { + reflowTime = $mdUtil.time(function() { + var animator = animatorFn || defaultAnimator; + animator(gridStyles.grid, gridStyles.tiles); + }); + return self; + }, + + /** + * Timing for the most recent layout run. + */ + performance: function() { + return { + tileCount: tileSpans.length, + layoutTime: layoutTime, + mapTime: mapTime, + reflowTime: reflowTime, + totalTime: layoutTime + mapTime + reflowTime + }; + } + }; + } + + /** + * Default Gridlist animator simple sets the css for each element; + * NOTE: any transitions effects must be manually set in the CSS. + * e.g. + * + * md-grid-tile { + * transition: all 700ms ease-out 50ms; + * } + * + */ + function GridTileAnimator(grid, tiles) { + grid.element.css(grid.style); + tiles.forEach(function(t) { + t.element.css(t.style); + }) + } + + /** + * Calculates the positions of tiles. + * + * The algorithm works as follows: + * An Array with length colCount (spaceTracker) keeps track of + * available tiling positions, where elements of value 0 represents an + * empty position. Space for a tile is reserved by finding a sequence of + * 0s with length <= than the tile's colspan. When such a space has been + * found, the occupied tile positions are incremented by the tile's + * rowspan value, as these positions have become unavailable for that + * many rows. + * + * If the end of a row has been reached without finding space for the + * tile, spaceTracker's elements are each decremented by 1 to a minimum + * of 0. Rows are searched in this fashion until space is found. + */ + function calculateGridFor(colCount, tileSpans) { + var curCol = 0, + curRow = 0, + spaceTracker = newSpaceTracker(); + + return { + positioning: tileSpans.map(function(spans, i) { + return { + spans: spans, + position: reserveSpace(spans, i) + }; + }), + rowCount: curRow + Math.max.apply(Math, spaceTracker) + }; + + function reserveSpace(spans, i) { + if (spans.col > colCount) { + throw 'md-grid-list: Tile at position ' + i + ' has a colspan ' + + '(' + spans.col + ') that exceeds the column count ' + + '(' + colCount + ')'; + } + + var start = 0, + end = 0; + + // TODO(shyndman): This loop isn't strictly necessary if you can + // determine the minimum number of rows before a space opens up. To do + // this, recognize that you've iterated across an entire row looking for + // space, and if so fast-forward by the minimum rowSpan count. Repeat + // until the required space opens up. + while (end - start < spans.col) { + if (curCol >= colCount) { + nextRow(); + continue; + } + + start = spaceTracker.indexOf(0, curCol); + if (start === -1 || (end = findEnd(start + 1)) === -1) { + start = end = 0; + nextRow(); + continue; + } + + curCol = end + 1; + } + + adjustRow(start, spans.col, spans.row); + curCol = start + spans.col; + + return { + col: start, + row: curRow + }; + } + + function nextRow() { + curCol = 0; + curRow++; + adjustRow(0, colCount, -1); // Decrement row spans by one + } + + function adjustRow(from, cols, by) { + for (var i = from; i < from + cols; i++) { + spaceTracker[i] = Math.max(spaceTracker[i] + by, 0); + } + } + + function findEnd(start) { + var i; + for (i = start; i < spaceTracker.length; i++) { + if (spaceTracker[i] !== 0) { + return i; + } + } + + if (i === spaceTracker.length) { + return i; + } + } + + function newSpaceTracker() { + var tracker = []; + for (var i = 0; i < colCount; i++) { + tracker.push(0); + } + return tracker; + } + } +} + +/** + * @ngdoc directive + * @name mdGridTile + * @module material.components.gridList + * @restrict E + * @description + * Tiles contain the content of an `md-grid-list`. They span one or more grid + * cells vertically or horizontally, and use `md-grid-tile-{footer,header}` to + * display secondary content. + * + * ### Responsive Attributes + * + * The `md-grid-tile` directive supports "responsive" attributes, which allow + * different `md-rowspan` and `md-colspan` values depending on the currently + * matching media query. + * + * In order to set a responsive attribute, first define the fallback value with + * the standard attribute name, then add additional attributes with the + * following convention: `{base-attribute-name}-{media-query-name}="{value}"` + * (ie. `md-colspan-sm="4"`) + * + * @param {number=} md-colspan The number of columns to span (default 1). Cannot + * exceed the number of columns in the grid. Supports interpolation. + * @param {number=} md-rowspan The number of rows to span (default 1). Supports + * interpolation. + * + * @usage + * With header: + * + * + * + *

      This is a header

      + *
      + *
      + *
      + * + * With footer: + * + * + * + *

      This is a footer

      + *
      + *
      + *
      + * + * Spanning multiple rows/columns: + * + * + * + * + * + * Responsive attributes: + * + * + * + * + */ +function GridTileDirective($mdMedia) { + return { + restrict: 'E', + require: '^mdGridList', + template: '
      ', + transclude: true, + scope: {}, + // Simple controller that exposes attributes to the grid directive + controller: ["$attrs", function($attrs) { + this.$attrs = $attrs; + }], + link: postLink + }; + + function postLink(scope, element, attrs, gridCtrl) { + // Apply semantics + element.attr('role', 'listitem'); + + // If our colspan or rowspan changes, trigger a layout + var unwatchAttrs = $mdMedia.watchResponsiveAttributes(['md-colspan', 'md-rowspan'], + attrs, angular.bind(gridCtrl, gridCtrl.invalidateLayout)); + + // Tile registration/deregistration + gridCtrl.invalidateTiles(); + scope.$on('$destroy', function() { + // Mark the tile as destroyed so it is no longer considered in layout, + // even if the DOM element sticks around (like during a leave animation) + element[0].$$mdDestroyed = true; + unwatchAttrs(); + gridCtrl.invalidateLayout(); + }); + + if (angular.isDefined(scope.$parent.$index)) { + scope.$watch(function() { return scope.$parent.$index; }, + function indexChanged(newIdx, oldIdx) { + if (newIdx === oldIdx) { + return; + } + gridCtrl.invalidateTiles(); + }); + } + } +} + + +function GridTileCaptionDirective() { + return { + template: '
      ', + transclude: true + }; +} + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.components.icon + * @description + * Icon + */ +angular.module('material.components.icon', ['material.core']); + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.components.input + */ +mdInputContainerDirective.$inject = ["$mdTheming", "$parse"]; +inputTextareaDirective.$inject = ["$mdUtil", "$window", "$mdAria", "$timeout", "$mdGesture"]; +mdMaxlengthDirective.$inject = ["$animate", "$mdUtil"]; +placeholderDirective.$inject = ["$compile"]; +ngMessageDirective.$inject = ["$mdUtil"]; +mdSelectOnFocusDirective.$inject = ["$timeout"]; +mdInputInvalidMessagesAnimation.$inject = ["$$AnimateRunner", "$animateCss", "$mdUtil"]; +ngMessagesAnimation.$inject = ["$$AnimateRunner", "$animateCss", "$mdUtil"]; +ngMessageAnimation.$inject = ["$$AnimateRunner", "$animateCss", "$mdUtil"]; +angular.module('material.components.input', [ + 'material.core' + ]) + .directive('mdInputContainer', mdInputContainerDirective) + .directive('label', labelDirective) + .directive('input', inputTextareaDirective) + .directive('textarea', inputTextareaDirective) + .directive('mdMaxlength', mdMaxlengthDirective) + .directive('placeholder', placeholderDirective) + .directive('ngMessages', ngMessagesDirective) + .directive('ngMessage', ngMessageDirective) + .directive('ngMessageExp', ngMessageDirective) + .directive('mdSelectOnFocus', mdSelectOnFocusDirective) + + .animation('.md-input-invalid', mdInputInvalidMessagesAnimation) + .animation('.md-input-messages-animation', ngMessagesAnimation) + .animation('.md-input-message-animation', ngMessageAnimation) + + // Register a service for each animation so that we can easily inject them into unit tests + .service('mdInputInvalidAnimation', mdInputInvalidMessagesAnimation) + .service('mdInputMessagesAnimation', ngMessagesAnimation) + .service('mdInputMessageAnimation', ngMessageAnimation); + +/** + * @ngdoc directive + * @name mdInputContainer + * @module material.components.input + * + * @restrict E + * + * @description + * `` is the parent of any input or textarea element. + * + * Input and textarea elements will not behave properly unless the md-input-container + * parent is provided. + * + * A single `` should contain only one `` element, otherwise it will throw an error. + * + * Exception: Hidden inputs (``) are ignored and will not throw an error, so + * you may combine these with other inputs. + * + * Note: When using `ngMessages` with your input element, make sure the message and container elements + * are *block* elements, otherwise animations applied to the messages will not look as intended. Either use a `div` and + * apply the `ng-message` and `ng-messages` classes respectively, or use the `md-block` class on your element. + * + * @param md-is-error {expression=} When the given expression evaluates to true, the input container + * will go into error state. Defaults to erroring if the input has been touched and is invalid. + * @param md-no-float {boolean=} When present, `placeholder` attributes on the input will not be converted to floating + * labels. + * + * @usage + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *

      When disabling floating labels

      + * + * + * + * + * + * + * + */ +function mdInputContainerDirective($mdTheming, $parse) { + + ContainerCtrl.$inject = ["$scope", "$element", "$attrs", "$animate"]; + var INPUT_TAGS = ['INPUT', 'TEXTAREA', 'SELECT', 'MD-SELECT']; + + var LEFT_SELECTORS = INPUT_TAGS.reduce(function(selectors, isel) { + return selectors.concat(['md-icon ~ ' + isel, '.md-icon ~ ' + isel]); + }, []).join(","); + + var RIGHT_SELECTORS = INPUT_TAGS.reduce(function(selectors, isel) { + return selectors.concat([isel + ' ~ md-icon', isel + ' ~ .md-icon']); + }, []).join(","); + + return { + restrict: 'E', + compile: compile, + controller: ContainerCtrl + }; + + function compile(tElement) { + // Check for both a left & right icon + var leftIcon = tElement[0].querySelector(LEFT_SELECTORS); + var rightIcon = tElement[0].querySelector(RIGHT_SELECTORS); + + if (leftIcon) { tElement.addClass('md-icon-left'); } + if (rightIcon) { tElement.addClass('md-icon-right'); } + + return function postLink(scope, element) { + $mdTheming(element); + }; + } + + function ContainerCtrl($scope, $element, $attrs, $animate) { + var self = this; + + self.isErrorGetter = $attrs.mdIsError && $parse($attrs.mdIsError); + + self.delegateClick = function() { + self.input.focus(); + }; + self.element = $element; + self.setFocused = function(isFocused) { + $element.toggleClass('md-input-focused', !!isFocused); + }; + self.setHasValue = function(hasValue) { + $element.toggleClass('md-input-has-value', !!hasValue); + }; + self.setHasPlaceholder = function(hasPlaceholder) { + $element.toggleClass('md-input-has-placeholder', !!hasPlaceholder); + }; + self.setInvalid = function(isInvalid) { + if (isInvalid) { + $animate.addClass($element, 'md-input-invalid'); + } else { + $animate.removeClass($element, 'md-input-invalid'); + } + }; + $scope.$watch(function() { + return self.label && self.input; + }, function(hasLabelAndInput) { + if (hasLabelAndInput && !self.label.attr('for')) { + self.label.attr('for', self.input.attr('id')); + } + }); + } +} + +function labelDirective() { + return { + restrict: 'E', + require: '^?mdInputContainer', + link: function(scope, element, attr, containerCtrl) { + if (!containerCtrl || attr.mdNoFloat || element.hasClass('md-container-ignore')) return; + + containerCtrl.label = element; + scope.$on('$destroy', function() { + containerCtrl.label = null; + }); + } + }; +} + +/** + * @ngdoc directive + * @name mdInput + * @restrict E + * @module material.components.input + * + * @description + * You can use any `` or ` + *
      + *
      This is required!
      + *
      That's too long!
      + *
      + *
      + * + * + * + * + * + * + * + * + * + *

      Notes

      + * + * - Requires [ngMessages](https://docs.angularjs.org/api/ngMessages). + * - Behaves like the [AngularJS input directive](https://docs.angularjs.org/api/ng/directive/input). + * + * The `md-input` and `md-input-container` directives use very specific positioning to achieve the + * error animation effects. Therefore, it is *not* advised to use the Layout system inside of the + * `` tags. Instead, use relative or absolute positioning. + * + * + *

      Textarea directive

      + * The `textarea` element within a `md-input-container` has the following specific behavior: + * - By default the `textarea` grows as the user types. This can be disabled via the `md-no-autogrow` + * attribute. + * - If a `textarea` has the `rows` attribute, it will treat the `rows` as the minimum height and will + * continue growing as the user types. For example a textarea with `rows="3"` will be 3 lines of text + * high initially. If no rows are specified, the directive defaults to 1. + * - The textarea's height gets set on initialization, as well as while the user is typing. In certain situations + * (e.g. while animating) the directive might have been initialized, before the element got it's final height. In + * those cases, you can trigger a resize manually by broadcasting a `md-resize-textarea` event on the scope. + * - If you wan't a `textarea` to stop growing at a certain point, you can specify the `max-rows` attribute. + * - The textarea's bottom border acts as a handle which users can drag, in order to resize the element vertically. + * Once the user has resized a `textarea`, the autogrowing functionality becomes disabled. If you don't want a + * `textarea` to be resizeable by the user, you can add the `md-no-resize` attribute. + */ + +function inputTextareaDirective($mdUtil, $window, $mdAria, $timeout, $mdGesture) { + return { + restrict: 'E', + require: ['^?mdInputContainer', '?ngModel', '?^form'], + link: postLink + }; + + function postLink(scope, element, attr, ctrls) { + + var containerCtrl = ctrls[0]; + var hasNgModel = !!ctrls[1]; + var ngModelCtrl = ctrls[1] || $mdUtil.fakeNgModel(); + var parentForm = ctrls[2]; + var isReadonly = angular.isDefined(attr.readonly); + var mdNoAsterisk = $mdUtil.parseAttributeBoolean(attr.mdNoAsterisk); + var tagName = element[0].tagName.toLowerCase(); + + + if (!containerCtrl) return; + if (attr.type === 'hidden') { + element.attr('aria-hidden', 'true'); + return; + } else if (containerCtrl.input) { + if (containerCtrl.input[0].contains(element[0])) { + return; + } else { + throw new Error(" can only have *one* , + * + * + * + */ +function mdSelectOnFocusDirective($timeout) { + + return { + restrict: 'A', + link: postLink + }; + + function postLink(scope, element, attr) { + if (element[0].nodeName !== 'INPUT' && element[0].nodeName !== "TEXTAREA") return; + + var preventMouseUp = false; + + element + .on('focus', onFocus) + .on('mouseup', onMouseUp); + + scope.$on('$destroy', function() { + element + .off('focus', onFocus) + .off('mouseup', onMouseUp); + }); + + function onFocus() { + preventMouseUp = true; + + $timeout(function() { + // Use HTMLInputElement#select to fix firefox select issues. + // The debounce is here for Edge's sake, otherwise the selection doesn't work. + element[0].select(); + + // This should be reset from inside the `focus`, because the event might + // have originated from something different than a click, e.g. a keyboard event. + preventMouseUp = false; + }, 1, false); + } + + // Prevents the default action of the first `mouseup` after a focus. + // This is necessary, because browsers fire a `mouseup` right after the element + // has been focused. In some browsers (Firefox in particular) this can clear the + // selection. There are examples of the problem in issue #7487. + function onMouseUp(event) { + if (preventMouseUp) { + event.preventDefault(); + } + } + } +} + +var visibilityDirectives = ['ngIf', 'ngShow', 'ngHide', 'ngSwitchWhen', 'ngSwitchDefault']; +function ngMessagesDirective() { + return { + restrict: 'EA', + link: postLink, + + // This is optional because we don't want target *all* ngMessage instances, just those inside of + // mdInputContainer. + require: '^^?mdInputContainer' + }; + + function postLink(scope, element, attrs, inputContainer) { + // If we are not a child of an input container, don't do anything + if (!inputContainer) return; + + // Add our animation class + element.toggleClass('md-input-messages-animation', true); + + // Add our md-auto-hide class to automatically hide/show messages when container is invalid + element.toggleClass('md-auto-hide', true); + + // If we see some known visibility directives, remove the md-auto-hide class + if (attrs.mdAutoHide == 'false' || hasVisibiltyDirective(attrs)) { + element.toggleClass('md-auto-hide', false); + } + } + + function hasVisibiltyDirective(attrs) { + return visibilityDirectives.some(function(attr) { + return attrs[attr]; + }); + } +} + +function ngMessageDirective($mdUtil) { + return { + restrict: 'EA', + compile: compile, + priority: 100 + }; + + function compile(tElement) { + if (!isInsideInputContainer(tElement)) { + + // When the current element is inside of a document fragment, then we need to check for an input-container + // in the postLink, because the element will be later added to the DOM and is currently just in a temporary + // fragment, which causes the input-container check to fail. + if (isInsideFragment()) { + return function (scope, element) { + if (isInsideInputContainer(element)) { + // Inside of the postLink function, a ngMessage directive will be a comment element, because it's + // currently hidden. To access the shown element, we need to use the element from the compile function. + initMessageElement(tElement); + } + }; + } + } else { + initMessageElement(tElement); + } + + function isInsideFragment() { + var nextNode = tElement[0]; + while (nextNode = nextNode.parentNode) { + if (nextNode.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { + return true; + } + } + return false; + } + + function isInsideInputContainer(element) { + return !!$mdUtil.getClosest(element, "md-input-container"); + } + + function initMessageElement(element) { + // Add our animation class + element.toggleClass('md-input-message-animation', true); + } + } +} + +var $$AnimateRunner, $animateCss, $mdUtil; + +function mdInputInvalidMessagesAnimation($$AnimateRunner, $animateCss, $mdUtil) { + saveSharedServices($$AnimateRunner, $animateCss, $mdUtil); + + return { + addClass: function(element, className, done) { + showInputMessages(element, done); + } + + // NOTE: We do not need the removeClass method, because the message ng-leave animation will fire + }; +} + +function ngMessagesAnimation($$AnimateRunner, $animateCss, $mdUtil) { + saveSharedServices($$AnimateRunner, $animateCss, $mdUtil); + + return { + enter: function(element, done) { + showInputMessages(element, done); + }, + + leave: function(element, done) { + hideInputMessages(element, done); + }, + + addClass: function(element, className, done) { + if (className == "ng-hide") { + hideInputMessages(element, done); + } else { + done(); + } + }, + + removeClass: function(element, className, done) { + if (className == "ng-hide") { + showInputMessages(element, done); + } else { + done(); + } + } + } +} + +function ngMessageAnimation($$AnimateRunner, $animateCss, $mdUtil) { + saveSharedServices($$AnimateRunner, $animateCss, $mdUtil); + + return { + enter: function(element, done) { + var animator = showMessage(element); + + animator.start().done(done); + }, + + leave: function(element, done) { + var animator = hideMessage(element); + + animator.start().done(done); + } + } +} + +function showInputMessages(element, done) { + var animators = [], animator; + var messages = getMessagesElement(element); + + angular.forEach(messages.children(), function(child) { + animator = showMessage(angular.element(child)); + + animators.push(animator.start()); + }); + + $$AnimateRunner.all(animators, done); +} + +function hideInputMessages(element, done) { + var animators = [], animator; + var messages = getMessagesElement(element); + + angular.forEach(messages.children(), function(child) { + animator = hideMessage(angular.element(child)); + + animators.push(animator.start()); + }); + + $$AnimateRunner.all(animators, done); +} + +function showMessage(element) { + var height = parseInt(window.getComputedStyle(element[0]).height); + var topMargin = parseInt(window.getComputedStyle(element[0]).marginTop); + + var messages = getMessagesElement(element); + var container = getInputElement(element); + + // Check to see if the message is already visible so we can skip + var alreadyVisible = (topMargin > -height); + + // If we have the md-auto-hide class, the md-input-invalid animation will fire, so we can skip + if (alreadyVisible || (messages.hasClass('md-auto-hide') && !container.hasClass('md-input-invalid'))) { + return $animateCss(element, {}); + } + + return $animateCss(element, { + event: 'enter', + structural: true, + from: {"opacity": 0, "margin-top": -height + "px"}, + to: {"opacity": 1, "margin-top": "0"}, + duration: 0.3 + }); +} + +function hideMessage(element) { + var height = element[0].offsetHeight; + var styles = window.getComputedStyle(element[0]); + + // If we are already hidden, just return an empty animation + if (styles.opacity == 0) { + return $animateCss(element, {}); + } + + // Otherwise, animate + return $animateCss(element, { + event: 'leave', + structural: true, + from: {"opacity": 1, "margin-top": 0}, + to: {"opacity": 0, "margin-top": -height + "px"}, + duration: 0.3 + }); +} + +function getInputElement(element) { + var inputContainer = element.controller('mdInputContainer'); + + return inputContainer.element; +} + +function getMessagesElement(element) { + // If we are a ng-message element, we need to traverse up the DOM tree + if (element.hasClass('md-input-message-animation')) { + return angular.element($mdUtil.getClosest(element, function(node) { + return node.classList.contains('md-input-messages-animation'); + })); + } + + // Otherwise, we can traverse down + return angular.element(element[0].querySelector('.md-input-messages-animation')); +} + +function saveSharedServices(_$$AnimateRunner_, _$animateCss_, _$mdUtil_) { + $$AnimateRunner = _$$AnimateRunner_; + $animateCss = _$animateCss_; + $mdUtil = _$mdUtil_; +} + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.components.list + * @description + * List module + */ +MdListController.$inject = ["$scope", "$element", "$mdListInkRipple"]; +mdListDirective.$inject = ["$mdTheming"]; +mdListItemDirective.$inject = ["$mdAria", "$mdConstant", "$mdUtil", "$timeout"]; +angular.module('material.components.list', [ + 'material.core' +]) + .controller('MdListController', MdListController) + .directive('mdList', mdListDirective) + .directive('mdListItem', mdListItemDirective); + +/** + * @ngdoc directive + * @name mdList + * @module material.components.list + * + * @restrict E + * + * @description + * The `` directive is a list container for 1..n `` tags. + * + * @usage + * + * + * + * + *
      + *

      {{item.title}}

      + *

      {{item.description}}

      + *
      + *
      + *
      + *
      + */ + +function mdListDirective($mdTheming) { + return { + restrict: 'E', + compile: function(tEl) { + tEl[0].setAttribute('role', 'list'); + return $mdTheming; + } + }; +} +/** + * @ngdoc directive + * @name mdListItem + * @module material.components.list + * + * @restrict E + * + * @description + * A `md-list-item` element can be used to represent some information in a row.
      + * + * @usage + * ### Single Row Item + * + * + * Single Row Item + * + * + * + * ### Multiple Lines + * By using the following markup, you will be able to have two lines inside of one `md-list-item`. + * + * + * + *
      + *

      First Line

      + *

      Second Line

      + *
      + *
      + *
      + * + * It is also possible to have three lines inside of one list item. + * + * + * + *
      + *

      First Line

      + *

      Second Line

      + *

      Third Line

      + *
      + *
      + *
      + * + * ### Secondary Items + * Secondary items are elements which will be aligned at the end of the `md-list-item`. + * + * + * + * Single Row Item + * + * Secondary Button + * + * + * + * + * It also possible to have multiple secondary items inside of one `md-list-item`. + * + * + * + * Single Row Item + * First Button + * Second Button + * + * + * + * ### Proxy Item + * Proxies are elements, which will execute their specific action on click
      + * Currently supported proxy items are + * - `md-checkbox` (Toggle) + * - `md-switch` (Toggle) + * - `md-menu` (Open) + * + * This means, when using a supported proxy item inside of `md-list-item`, the list item will + * become clickable and executes the associated action of the proxy element on click. + * + * + * + * First Line + * + * + * + * + * The `md-checkbox` element will be automatically detected as a proxy element and will toggle on click. + * + * + * + * First Line + * + * + * + * + * The recognized `md-switch` will toggle its state, when the user clicks on the `md-list-item`. + * + * It is also possible to have a `md-menu` inside of a `md-list-item`. + * + * + *

      Click anywhere to fire the secondary action

      + * + * + * + * + * + * + * + * Redial + * + * + * + * + * Check voicemail + * + * + * + * + * + * Notifications + * + * + * + * + *
      + *
      + * + * The menu will automatically open, when the users clicks on the `md-list-item`.
      + * + * If the developer didn't specify any position mode on the menu, the `md-list-item` will automatically detect the + * position mode and applies it to the `md-menu`. + * + * ### Avatars + * Sometimes you may want to have some avatars inside of the `md-list-item `.
      + * You are able to create a optimized icon for the list item, by applying the `.md-avatar` class on the `` element. + * + * + * + * + * Alan Turing + * + * + * When using `` for an avater, you have to use the `.md-avatar-icon` class. + * + * + * + * Timothy Kopra + * + * + * + * In cases, you have a `md-list-item`, which doesn't have any avatar, + * but you want to align it with the other avatar items, you have to use the `.md-offset` class. + * + * + * + * Jon Doe + * + * + * + * ### DOM modification + * The `md-list-item` component automatically detects if the list item should be clickable. + * + * --- + * If the `md-list-item` is clickable, we wrap all content inside of a `
      ` and create + * an overlaying button, which will will execute the given actions (like `ng-href`, `ng-click`) + * + * We create an overlaying button, instead of wrapping all content inside of the button, + * because otherwise some elements may not be clickable inside of the button. + * + * --- + * When using a secondary item inside of your list item, the `md-list-item` component will automatically create + * a secondary container at the end of the `md-list-item`, which contains all secondary items. + * + * The secondary item container is not static, because otherwise the overflow will not work properly on the + * list item. + * + */ +function mdListItemDirective($mdAria, $mdConstant, $mdUtil, $timeout) { + var proxiedTypes = ['md-checkbox', 'md-switch', 'md-menu']; + return { + restrict: 'E', + controller: 'MdListController', + compile: function(tEl, tAttrs) { + + // Check for proxy controls (no ng-click on parent, and a control inside) + var secondaryItems = tEl[0].querySelectorAll('.md-secondary'); + var hasProxiedElement; + var proxyElement; + var itemContainer = tEl; + + tEl[0].setAttribute('role', 'listitem'); + + if (tAttrs.ngClick || tAttrs.ngDblclick || tAttrs.ngHref || tAttrs.href || tAttrs.uiSref || tAttrs.ngAttrUiSref) { + wrapIn('button'); + } else { + for (var i = 0, type; type = proxiedTypes[i]; ++i) { + if (proxyElement = tEl[0].querySelector(type)) { + hasProxiedElement = true; + break; + } + } + if (hasProxiedElement) { + wrapIn('div'); + } else if (!tEl[0].querySelector('md-button:not(.md-secondary):not(.md-exclude)')) { + tEl.addClass('md-no-proxy'); + } + } + + wrapSecondaryItems(); + setupToggleAria(); + + if (hasProxiedElement && proxyElement.nodeName === "MD-MENU") { + setupProxiedMenu(); + } + + function setupToggleAria() { + var toggleTypes = ['md-switch', 'md-checkbox']; + var toggle; + + for (var i = 0, toggleType; toggleType = toggleTypes[i]; ++i) { + if (toggle = tEl.find(toggleType)[0]) { + if (!toggle.hasAttribute('aria-label')) { + var p = tEl.find('p')[0]; + if (!p) return; + toggle.setAttribute('aria-label', 'Toggle ' + p.textContent); + } + } + } + } + + function setupProxiedMenu() { + var menuEl = angular.element(proxyElement); + + var isEndAligned = menuEl.parent().hasClass('md-secondary-container') || + proxyElement.parentNode.firstElementChild !== proxyElement; + + var xAxisPosition = 'left'; + + if (isEndAligned) { + // When the proxy item is aligned at the end of the list, we have to set the origin to the end. + xAxisPosition = 'right'; + } + + // Set the position mode / origin of the proxied menu. + if (!menuEl.attr('md-position-mode')) { + menuEl.attr('md-position-mode', xAxisPosition + ' target'); + } + + // Apply menu open binding to menu button + var menuOpenButton = menuEl.children().eq(0); + if (!hasClickEvent(menuOpenButton[0])) { + menuOpenButton.attr('ng-click', '$mdOpenMenu($event)'); + } + + if (!menuOpenButton.attr('aria-label')) { + menuOpenButton.attr('aria-label', 'Open List Menu'); + } + } + + function wrapIn(type) { + if (type == 'div') { + itemContainer = angular.element('
      '); + itemContainer.append(tEl.contents()); + tEl.addClass('md-proxy-focus'); + } else { + // Element which holds the default list-item content. + itemContainer = angular.element( + '
      '+ + '
      '+ + '
      ' + ); + + // Button which shows ripple and executes primary action. + var buttonWrap = angular.element( + '' + ); + + buttonWrap[0].setAttribute('aria-label', tEl[0].textContent); + + copyAttributes(tEl[0], buttonWrap[0]); + + // We allow developers to specify the `md-no-focus` class, to disable the focus style + // on the button executor. Once more classes should be forwarded, we should probably make the + // class forward more generic. + if (tEl.hasClass('md-no-focus')) { + buttonWrap.addClass('md-no-focus'); + } + + // Append the button wrap before our list-item content, because it will overlay in relative. + itemContainer.prepend(buttonWrap); + itemContainer.children().eq(1).append(tEl.contents()); + + tEl.addClass('_md-button-wrap'); + } + + tEl[0].setAttribute('tabindex', '-1'); + tEl.append(itemContainer); + } + + function wrapSecondaryItems() { + var secondaryItemsWrapper = angular.element('
      '); + + angular.forEach(secondaryItems, function(secondaryItem) { + wrapSecondaryItem(secondaryItem, secondaryItemsWrapper); + }); + + itemContainer.append(secondaryItemsWrapper); + } + + function wrapSecondaryItem(secondaryItem, container) { + // If the current secondary item is not a button, but contains a ng-click attribute, + // the secondary item will be automatically wrapped inside of a button. + if (secondaryItem && !isButton(secondaryItem) && secondaryItem.hasAttribute('ng-click')) { + + $mdAria.expect(secondaryItem, 'aria-label'); + var buttonWrapper = angular.element(''); + + // Copy the attributes from the secondary item to the generated button. + // We also support some additional attributes from the secondary item, + // because some developers may use a ngIf, ngHide, ngShow on their item. + copyAttributes(secondaryItem, buttonWrapper[0], ['ng-if', 'ng-hide', 'ng-show']); + + secondaryItem.setAttribute('tabindex', '-1'); + buttonWrapper.append(secondaryItem); + + secondaryItem = buttonWrapper[0]; + } + + if (secondaryItem && (!hasClickEvent(secondaryItem) || (!tAttrs.ngClick && isProxiedElement(secondaryItem)))) { + // In this case we remove the secondary class, so we can identify it later, when we searching for the + // proxy items. + angular.element(secondaryItem).removeClass('md-secondary'); + } + + tEl.addClass('md-with-secondary'); + container.append(secondaryItem); + } + + /** + * Copies attributes from a source element to the destination element + * By default the function will copy the most necessary attributes, supported + * by the button executor for clickable list items. + * @param source Element with the specified attributes + * @param destination Element which will retrieve the attributes + * @param extraAttrs Additional attributes, which will be copied over. + */ + function copyAttributes(source, destination, extraAttrs) { + var copiedAttrs = $mdUtil.prefixer([ + 'ng-if', 'ng-click', 'ng-dblclick', 'aria-label', 'ng-disabled', 'ui-sref', + 'href', 'ng-href', 'target', 'ng-attr-ui-sref', 'ui-sref-opts' + ]); + + if (extraAttrs) { + copiedAttrs = copiedAttrs.concat($mdUtil.prefixer(extraAttrs)); + } + + angular.forEach(copiedAttrs, function(attr) { + if (source.hasAttribute(attr)) { + destination.setAttribute(attr, source.getAttribute(attr)); + source.removeAttribute(attr); + } + }); + } + + function isProxiedElement(el) { + return proxiedTypes.indexOf(el.nodeName.toLowerCase()) != -1; + } + + function isButton(el) { + var nodeName = el.nodeName.toUpperCase(); + + return nodeName == "MD-BUTTON" || nodeName == "BUTTON"; + } + + function hasClickEvent (element) { + var attr = element.attributes; + for (var i = 0; i < attr.length; i++) { + if (tAttrs.$normalize(attr[i].name) === 'ngClick') return true; + } + return false; + } + + return postLink; + + function postLink($scope, $element, $attr, ctrl) { + $element.addClass('_md'); // private md component indicator for styling + + var proxies = [], + firstElement = $element[0].firstElementChild, + isButtonWrap = $element.hasClass('_md-button-wrap'), + clickChild = isButtonWrap ? firstElement.firstElementChild : firstElement, + hasClick = clickChild && hasClickEvent(clickChild); + + computeProxies(); + computeClickable(); + + if ($element.hasClass('md-proxy-focus') && proxies.length) { + angular.forEach(proxies, function(proxy) { + proxy = angular.element(proxy); + + $scope.mouseActive = false; + proxy.on('mousedown', function() { + $scope.mouseActive = true; + $timeout(function(){ + $scope.mouseActive = false; + }, 100); + }) + .on('focus', function() { + if ($scope.mouseActive === false) { $element.addClass('md-focused'); } + proxy.on('blur', function proxyOnBlur() { + $element.removeClass('md-focused'); + proxy.off('blur', proxyOnBlur); + }); + }); + }); + } + + + function computeProxies() { + if (firstElement && firstElement.children && !hasClick) { + + angular.forEach(proxiedTypes, function(type) { + + // All elements which are not capable for being used a proxy have the .md-secondary class + // applied. These items had been sorted out in the secondary wrap function. + angular.forEach(firstElement.querySelectorAll(type + ':not(.md-secondary)'), function(child) { + proxies.push(child); + }); + }); + + } + } + + function computeClickable() { + if (proxies.length == 1 || hasClick) { + $element.addClass('md-clickable'); + + if (!hasClick) { + ctrl.attachRipple($scope, angular.element($element[0].querySelector('.md-no-style'))); + } + } + } + + function isEventFromControl(event) { + var forbiddenControls = ['md-slider']; + + // If there is no path property in the event, then we can assume that the event was not bubbled. + if (!event.path) { + return forbiddenControls.indexOf(event.target.tagName.toLowerCase()) !== -1; + } + + // We iterate the event path up and check for a possible component. + // Our maximum index to search, is the list item root. + var maxPath = event.path.indexOf($element.children()[0]); + + for (var i = 0; i < maxPath; i++) { + if (forbiddenControls.indexOf(event.path[i].tagName.toLowerCase()) !== -1) { + return true; + } + } + } + + var clickChildKeypressListener = function(e) { + if (e.target.nodeName != 'INPUT' && e.target.nodeName != 'TEXTAREA' && !e.target.isContentEditable) { + var keyCode = e.which || e.keyCode; + if (keyCode == $mdConstant.KEY_CODE.SPACE) { + if (clickChild) { + clickChild.click(); + e.preventDefault(); + e.stopPropagation(); + } + } + } + }; + + if (!hasClick && !proxies.length) { + clickChild && clickChild.addEventListener('keypress', clickChildKeypressListener); + } + + $element.off('click'); + $element.off('keypress'); + + if (proxies.length == 1 && clickChild) { + $element.children().eq(0).on('click', function(e) { + // When the event is coming from an control and it should not trigger the proxied element + // then we are skipping. + if (isEventFromControl(e)) return; + + var parentButton = $mdUtil.getClosest(e.target, 'BUTTON'); + if (!parentButton && clickChild.contains(e.target)) { + angular.forEach(proxies, function(proxy) { + if (e.target !== proxy && !proxy.contains(e.target)) { + if (proxy.nodeName === 'MD-MENU') { + proxy = proxy.children[0]; + } + angular.element(proxy).triggerHandler('click'); + } + }); + } + }); + } + + $scope.$on('$destroy', function () { + clickChild && clickChild.removeEventListener('keypress', clickChildKeypressListener); + }); + } + } + }; +} + +/* + * @private + * @ngdoc controller + * @name MdListController + * @module material.components.list + * + */ +function MdListController($scope, $element, $mdListInkRipple) { + var ctrl = this; + ctrl.attachRipple = attachRipple; + + function attachRipple (scope, element) { + var options = {}; + $mdListInkRipple.attach(scope, element, options); + } +} + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.components.menu + */ + +angular.module('material.components.menu', [ + 'material.core', + 'material.components.backdrop' +]); + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.components.menu-bar + */ + +angular.module('material.components.menuBar', [ + 'material.core', + 'material.components.icon', + 'material.components.menu' +]); + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.components.navBar + */ + + +MdNavBarController.$inject = ["$element", "$scope", "$timeout", "$mdConstant"]; +MdNavItem.$inject = ["$$rAF"]; +MdNavItemController.$inject = ["$element"]; +MdNavBar.$inject = ["$mdAria", "$mdTheming"]; +angular.module('material.components.navBar', ['material.core']) + .controller('MdNavBarController', MdNavBarController) + .directive('mdNavBar', MdNavBar) + .controller('MdNavItemController', MdNavItemController) + .directive('mdNavItem', MdNavItem); + + +/***************************************************************************** + * PUBLIC DOCUMENTATION * + *****************************************************************************/ +/** + * @ngdoc directive + * @name mdNavBar + * @module material.components.navBar + * + * @restrict E + * + * @description + * The `` directive renders a list of material tabs that can be used + * for top-level page navigation. Unlike ``, it has no concept of a tab + * body and no bar pagination. + * + * Because it deals with page navigation, certain routing concepts are built-in. + * Route changes via via ng-href, ui-sref, or ng-click events are supported. + * Alternatively, the user could simply watch currentNavItem for changes. + * + * Accessibility functionality is implemented as a site navigator with a + * listbox, according to + * https://www.w3.org/TR/wai-aria-practices/#Site_Navigator_Tabbed_Style + * + * @param {string=} mdSelectedNavItem The name of the current tab; this must + * match the name attribute of `` + * @param {string=} navBarAriaLabel An aria-label for the nav-bar + * + * @usage + * + * + * Page One + * Page Two + * Page Three + * + * + * + * (function() { + * ‘use strict’; + * + * $rootScope.$on('$routeChangeSuccess', function(event, current) { + * $scope.currentLink = getCurrentLinkFromRoute(current); + * }); + * }); + * + */ + +/***************************************************************************** + * mdNavItem + *****************************************************************************/ +/** + * @ngdoc directive + * @name mdNavItem + * @module material.components.navBar + * + * @restrict E + * + * @description + * `` describes a page navigation link within the `` + * component. It renders an md-button as the actual link. + * + * Exactly one of the mdNavClick, mdNavHref, mdNavSref attributes are required to be + * specified. + * + * @param {Function=} mdNavClick Function which will be called when the + * link is clicked to change the page. Renders as an `ng-click`. + * @param {string=} mdNavHref url to transition to when this link is clicked. + * Renders as an `ng-href`. + * @param {string=} mdNavSref Ui-router state to transition to when this link is + * clicked. Renders as a `ui-sref`. + * @param {string=} name The name of this link. Used by the nav bar to know + * which link is currently selected. + * + * @usage + * See `` for usage. + */ + + +/***************************************************************************** + * IMPLEMENTATION * + *****************************************************************************/ + +function MdNavBar($mdAria, $mdTheming) { + return { + restrict: 'E', + transclude: true, + controller: MdNavBarController, + controllerAs: 'ctrl', + bindToController: true, + scope: { + 'mdSelectedNavItem': '=?', + 'navBarAriaLabel': '@?', + }, + template: + '
      ' + + '' + + '' + + '
      ', + link: function(scope, element, attrs, ctrl) { + $mdTheming(element); + if (!ctrl.navBarAriaLabel) { + $mdAria.expectAsync(element, 'aria-label', angular.noop); + } + }, + }; +} + +/** + * Controller for the nav-bar component. + * + * Accessibility functionality is implemented as a site navigator with a + * listbox, according to + * https://www.w3.org/TR/wai-aria-practices/#Site_Navigator_Tabbed_Style + * @param {!angular.JQLite} $element + * @param {!angular.Scope} $scope + * @param {!angular.Timeout} $timeout + * @param {!Object} $mdConstant + * @constructor + * @final + * @ngInject + */ +function MdNavBarController($element, $scope, $timeout, $mdConstant) { + // Injected variables + /** @private @const {!angular.Timeout} */ + this._$timeout = $timeout; + + /** @private @const {!angular.Scope} */ + this._$scope = $scope; + + /** @private @const {!Object} */ + this._$mdConstant = $mdConstant; + + // Data-bound variables. + /** @type {string} */ + this.mdSelectedNavItem; + + /** @type {string} */ + this.navBarAriaLabel; + + // State variables. + + /** @type {?angular.JQLite} */ + this._navBarEl = $element[0]; + + /** @type {?angular.JQLite} */ + this._inkbar; + + var self = this; + // need to wait for transcluded content to be available + var deregisterTabWatch = this._$scope.$watch(function() { + return self._navBarEl.querySelectorAll('._md-nav-button').length; + }, + function(newLength) { + if (newLength > 0) { + self._initTabs(); + deregisterTabWatch(); + } + }); +} + + + +/** + * Initializes the tab components once they exist. + * @private + */ +MdNavBarController.prototype._initTabs = function() { + this._inkbar = angular.element(this._navBarEl.getElementsByTagName('md-nav-ink-bar')[0]); + + var self = this; + this._$timeout(function() { + self._updateTabs(self.mdSelectedNavItem, undefined); + }); + + this._$scope.$watch('ctrl.mdSelectedNavItem', function(newValue, oldValue) { + // Wait a digest before update tabs for products doing + // anything dynamic in the template. + self._$timeout(function() { + self._updateTabs(newValue, oldValue); + }); + }); +}; + +/** + * Set the current tab to be selected. + * @param {string|undefined} newValue New current tab name. + * @param {string|undefined} oldValue Previous tab name. + * @private + */ +MdNavBarController.prototype._updateTabs = function(newValue, oldValue) { + var self = this; + var tabs = this._getTabs(); + var oldIndex = -1; + var newIndex = -1; + var newTab = this._getTabByName(newValue); + var oldTab = this._getTabByName(oldValue); + + if (oldTab) { + oldTab.setSelected(false); + oldIndex = tabs.indexOf(oldTab); + } + + if (newTab) { + newTab.setSelected(true); + newIndex = tabs.indexOf(newTab); + } + + this._$timeout(function() { + self._updateInkBarStyles(newTab, newIndex, oldIndex); + }); +}; + +/** + * Repositions the ink bar to the selected tab. + * @private + */ +MdNavBarController.prototype._updateInkBarStyles = function(tab, newIndex, oldIndex) { + this._inkbar.toggleClass('_md-left', newIndex < oldIndex) + .toggleClass('_md-right', newIndex > oldIndex); + + this._inkbar.css({display: newIndex < 0 ? 'none' : ''}); + + if(tab){ + var tabEl = tab.getButtonEl(); + var left = tabEl.offsetLeft; + + this._inkbar.css({left: left + 'px', width: tabEl.offsetWidth + 'px'}); + } +}; + +/** + * Returns an array of the current tabs. + * @return {!Array} + * @private + */ +MdNavBarController.prototype._getTabs = function() { + var linkArray = Array.prototype.slice.call( + this._navBarEl.querySelectorAll('.md-nav-item')); + return linkArray.map(function(el) { + return angular.element(el).controller('mdNavItem') + }); +}; + +/** + * Returns the tab with the specified name. + * @param {string} name The name of the tab, found in its name attribute. + * @return {!NavItemController|undefined} + * @private + */ +MdNavBarController.prototype._getTabByName = function(name) { + return this._findTab(function(tab) { + return tab.getName() == name; + }); +}; + +/** + * Returns the selected tab. + * @return {!NavItemController|undefined} + * @private + */ +MdNavBarController.prototype._getSelectedTab = function() { + return this._findTab(function(tab) { + return tab.isSelected() + }); +}; + +/** + * Returns the focused tab. + * @return {!NavItemController|undefined} + */ +MdNavBarController.prototype.getFocusedTab = function() { + return this._findTab(function(tab) { + return tab.hasFocus() + }); +}; + +/** + * Find a tab that matches the specified function. + * @private + */ +MdNavBarController.prototype._findTab = function(fn) { + var tabs = this._getTabs(); + for (var i = 0; i < tabs.length; i++) { + if (fn(tabs[i])) { + return tabs[i]; + } + } + + return null; +}; + +/** + * Direct focus to the selected tab when focus enters the nav bar. + */ +MdNavBarController.prototype.onFocus = function() { + var tab = this._getSelectedTab(); + if (tab) { + tab.setFocused(true); + } +}; + +/** + * Clear tab focus when focus leaves the nav bar. + */ +MdNavBarController.prototype.onBlur = function() { + var tab = this.getFocusedTab(); + if (tab) { + tab.setFocused(false); + } +}; + +/** + * Move focus from oldTab to newTab. + * @param {!NavItemController} oldTab + * @param {!NavItemController} newTab + * @private + */ +MdNavBarController.prototype._moveFocus = function(oldTab, newTab) { + oldTab.setFocused(false); + newTab.setFocused(true); +}; + +/** + * Responds to keypress events. + * @param {!Event} e + */ +MdNavBarController.prototype.onKeydown = function(e) { + var keyCodes = this._$mdConstant.KEY_CODE; + var tabs = this._getTabs(); + var focusedTab = this.getFocusedTab(); + if (!focusedTab) return; + + var focusedTabIndex = tabs.indexOf(focusedTab); + + // use arrow keys to navigate between tabs + switch (e.keyCode) { + case keyCodes.UP_ARROW: + case keyCodes.LEFT_ARROW: + if (focusedTabIndex > 0) { + this._moveFocus(focusedTab, tabs[focusedTabIndex - 1]); + } + break; + case keyCodes.DOWN_ARROW: + case keyCodes.RIGHT_ARROW: + if (focusedTabIndex < tabs.length - 1) { + this._moveFocus(focusedTab, tabs[focusedTabIndex + 1]); + } + break; + case keyCodes.SPACE: + case keyCodes.ENTER: + // timeout to avoid a "digest already in progress" console error + this._$timeout(function() { + focusedTab.getButtonEl().click(); + }); + break; + } +}; + +/** + * @ngInject + */ +function MdNavItem($$rAF) { + return { + restrict: 'E', + require: ['mdNavItem', '^mdNavBar'], + controller: MdNavItemController, + bindToController: true, + controllerAs: 'ctrl', + replace: true, + transclude: true, + template: + '
    • ' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
    • ', + scope: { + 'mdNavClick': '&?', + 'mdNavHref': '@?', + 'mdNavSref': '@?', + 'name': '@', + }, + link: function(scope, element, attrs, controllers) { + var mdNavItem = controllers[0]; + var mdNavBar = controllers[1]; + + // When accessing the element's contents synchronously, they + // may not be defined yet because of transclusion. There is a higher chance + // that it will be accessible if we wait one frame. + $$rAF(function() { + if (!mdNavItem.name) { + mdNavItem.name = angular.element(element[0].querySelector('._md-nav-button-text')) + .text().trim(); + } + + var navButton = angular.element(element[0].querySelector('._md-nav-button')); + navButton.on('click', function() { + mdNavBar.mdSelectedNavItem = mdNavItem.name; + scope.$apply(); + }); + }); + } + }; +} + +/** + * Controller for the nav-item component. + * @param {!angular.JQLite} $element + * @constructor + * @final + * @ngInject + */ +function MdNavItemController($element) { + + /** @private @const {!angular.JQLite} */ + this._$element = $element; + + // Data-bound variables + /** @const {?Function} */ + this.mdNavClick; + /** @const {?string} */ + this.mdNavHref; + /** @const {?string} */ + this.name; + + // State variables + /** @private {boolean} */ + this._selected = false; + + /** @private {boolean} */ + this._focused = false; + + var hasNavClick = !!($element.attr('md-nav-click')); + var hasNavHref = !!($element.attr('md-nav-href')); + var hasNavSref = !!($element.attr('md-nav-sref')); + + // Cannot specify more than one nav attribute + if ((hasNavClick ? 1:0) + (hasNavHref ? 1:0) + (hasNavSref ? 1:0) > 1) { + throw Error( + 'Must specify exactly one of md-nav-click, md-nav-href, ' + + 'md-nav-sref for nav-item directive'); + } +} + +/** + * Returns a map of class names and values for use by ng-class. + * @return {!Object} + */ +MdNavItemController.prototype.getNgClassMap = function() { + return { + 'md-active': this._selected, + 'md-primary': this._selected, + 'md-unselected': !this._selected, + 'md-focused': this._focused, + }; +}; + +/** + * Get the name attribute of the tab. + * @return {string} + */ +MdNavItemController.prototype.getName = function() { + return this.name; +}; + +/** + * Get the button element associated with the tab. + * @return {!Element} + */ +MdNavItemController.prototype.getButtonEl = function() { + return this._$element[0].querySelector('._md-nav-button'); +}; + +/** + * Set the selected state of the tab. + * @param {boolean} isSelected + */ +MdNavItemController.prototype.setSelected = function(isSelected) { + this._selected = isSelected; +}; + +/** + * @return {boolean} + */ +MdNavItemController.prototype.isSelected = function() { + return this._selected; +}; + +/** + * Set the focused state of the tab. + * @param {boolean} isFocused + */ +MdNavItemController.prototype.setFocused = function(isFocused) { + this._focused = isFocused; +}; + +/** + * @return {boolean} + */ +MdNavItemController.prototype.hasFocus = function() { + return this._focused; +}; + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.components.panel + */ +MdPanelService.$inject = ["$rootElement", "$rootScope", "$injector", "$window"]; +angular + .module('material.components.panel', [ + 'material.core', + 'material.components.backdrop' + ]) + .service('$mdPanel', MdPanelService); + + +/***************************************************************************** + * PUBLIC DOCUMENTATION * + *****************************************************************************/ + +/** + * @ngdoc service + * @name $mdPanel + * @module material.components.panel + * + * @description + * `$mdPanel` is a robust, low-level service for creating floating panels on + * the screen. It can be used to implement tooltips, dialogs, pop-ups, etc. + * + * @usage + * + * (function(angular, undefined) { + * ‘use strict’; + * + * angular + * .module('demoApp', ['ngMaterial']) + * .controller('DemoDialogController', DialogController); + * + * var panelRef; + * + * function showPanel($event) { + * var panelPosition = $mdPanel.newPanelPosition() + * .absolute() + * .top('50%') + * .left('50%'); + * + * var panelAnimation = $mdPanel.newPanelAnimation() + * .targetEvent($event) + * .defaultAnimation('md-panel-animate-fly') + * .closeTo('.show-button'); + * + * var config = { + * attachTo: angular.element(document.body), + * controller: DialogController, + * controllerAs: 'ctrl', + * position: panelPosition, + * animation: panelAnimation, + * targetEvent: $event, + * templateUrl: 'dialog-template.html', + * clickOutsideToClose: true, + * escapeToClose: true, + * focusOnOpen: true + * } + * + * $mdPanel.open(config) + * .then(function(result) { + * panelRef = result; + * }); + * } + * + * function DialogController(MdPanelRef, toppings) { + * var toppings; + * + * function closeDialog() { + * MdPanelRef && MdPanelRef.close(); + * } + * } + * })(angular); + * + */ + +/** + * @ngdoc method + * @name $mdPanel#create + * @description + * Creates a panel with the specified options. + * + * @param config {!Object=} Specific configuration object that may contain the + * following properties: + * + * - `id` - `{string=}`: An ID to track the panel by. When an ID is provided, + * the created panel is added to a tracked panels object. Any subsequent + * requests made to create a panel with that ID are ignored. This is useful + * in having the panel service not open multiple panels from the same user + * interaction when there is no backdrop and events are propagated. Defaults + * to an arbitrary string that is not tracked. + * - `template` - `{string=}`: HTML template to show in the panel. This + * **must** be trusted HTML with respect to Angular’s + * [$sce service](https://docs.angularjs.org/api/ng/service/$sce). + * - `templateUrl` - `{string=}`: The URL that will be used as the content of + * the panel. + * - `controller` - `{(function|string)=}`: The controller to associate with + * the panel. The controller can inject a reference to the returned + * panelRef, which allows the panel to be closed, hidden, and shown. Any + * fields passed in through locals or resolve will be bound to the + * controller. + * - `controllerAs` - `{string=}`: An alias to assign the controller to on + * the scope. + * - `bindToController` - `{boolean=}`: Binds locals to the controller + * instead of passing them in. Defaults to true, as this is a best + * practice. + * - `locals` - `{Object=}`: An object containing key/value pairs. The keys + * will be used as names of values to inject into the controller. For + * example, `locals: {three: 3}` would inject `three` into the controller, + * with the value 3. + * - `resolve` - `{Object=}`: Similar to locals, except it takes promises as + * values. The panel will not open until all of the promises resolve. + * - `attachTo` - `{(string|!angular.JQLite|!Element)=}`: The element to + * attach the panel to. Defaults to appending to the root element of the + * application. + * - `propagateContainerEvents` - `{boolean=}`: Whether pointer or touch + * events should be allowed to propagate 'go through' the container, aka the + * wrapper, of the panel. Defaults to false. + * - `panelClass` - `{string=}`: A css class to apply to the panel element. + * This class should define any borders, box-shadow, etc. for the panel. + * - `zIndex` - `{number=}`: The z-index to place the panel at. + * Defaults to 80. + * - `position` - `{MdPanelPosition=}`: An MdPanelPosition object that + * specifies the alignment of the panel. For more information, see + * `MdPanelPosition`. + * - `clickOutsideToClose` - `{boolean=}`: Whether the user can click + * outside the panel to close it. Defaults to false. + * - `escapeToClose` - `{boolean=}`: Whether the user can press escape to + * close the panel. Defaults to false. + * - `trapFocus` - `{boolean=}`: Whether focus should be trapped within the + * panel. If `trapFocus` is true, the user will not be able to interact + * with the rest of the page until the panel is dismissed. Defaults to + * false. + * - `focusOnOpen` - `{boolean=}`: An option to override focus behavior on + * open. Only disable if focusing some other way, as focus management is + * required for panels to be accessible. Defaults to true. + * - `fullscreen` - `{boolean=}`: Whether the panel should be full screen. + * Applies the class `._md-panel-fullscreen` to the panel on open. Defaults + * to false. + * - `animation` - `{MdPanelAnimation=}`: An MdPanelAnimation object that + * specifies the animation of the panel. For more information, see + * `MdPanelAnimation`. + * - `hasBackdrop` - `{boolean=}`: Whether there should be an opaque backdrop + * behind the panel. Defaults to false. + * - `disableParentScroll` - `{boolean=}`: Whether the user can scroll the + * page behind the panel. Defaults to false. + * - `onDomAdded` - `{function=}`: Callback function used to announce when + * the panel is added to the DOM. + * - `onOpenComplete` - `{function=}`: Callback function used to announce + * when the open() action is finished. + * - `onRemoving` - `{function=}`: Callback function used to announce the + * close/hide() action is starting. + * - `onDomRemoved` - `{function=}`: Callback function used to announce when + * the panel is removed from the DOM. + * - `origin` - `{(string|!angular.JQLite|!Element)=}`: The element to focus + * on when the panel closes. This is commonly the element which triggered + * the opening of the panel. If you do not use `origin`, you need to control + * the focus manually. + * + * @returns {!MdPanelRef} panelRef + */ + + +/** + * @ngdoc method + * @name $mdPanel#open + * @description + * Calls the create method above, then opens the panel. This is a shortcut for + * creating and then calling open manually. If custom methods need to be + * called when the panel is added to the DOM or opened, do not use this method. + * Instead create the panel, chain promises on the domAdded and openComplete + * methods, and call open from the returned panelRef. + * + * @param {!Object=} config Specific configuration object that may contain + * the properties defined in `$mdPanel.create`. + * @returns {!angular.$q.Promise} panelRef A promise that resolves + * to an instance of the panel. + */ + + +/** + * @ngdoc method + * @name $mdPanel#newPanelPosition + * @description + * Returns a new instance of the MdPanelPosition object. Use this to create + * the position config object. + * + * @returns {!MdPanelPosition} panelPosition + */ + + +/** + * @ngdoc method + * @name $mdPanel#newPanelAnimation + * @description + * Returns a new instance of the MdPanelAnimation object. Use this to create + * the animation config object. + * + * @returns {!MdPanelAnimation} panelAnimation + */ + + +/***************************************************************************** + * MdPanelRef * + *****************************************************************************/ + + +/** + * @ngdoc type + * @name MdPanelRef + * @module material.components.panel + * @description + * A reference to a created panel. This reference contains a unique id for the + * panel, along with the following properties: + * + * - `id` - `{string}`: The unique id for the panel. This id is used to track + * when a panel was interacted with. + * - `config` - `{!Object=}`: The entire config object that was used in + * create. + * - `isAttached` - `{boolean}`: Whether the panel is attached to the DOM. + * Visibility to the user does not factor into isAttached. + * - `panelContainer` - `{angular.JQLite}`: The wrapper element containing the + * panel. This property is added in order to have access to the `addClass`, + * `removeClass`, `toggleClass`, etc methods. + * - `panelEl` - `{angular.JQLite}`: The panel element. This property is added + * in order to have access to the `addClass`, `removeClass`, `toggleClass`, + * etc methods. + */ + +/** + * @ngdoc method + * @name MdPanelRef#open + * @description + * Attaches and shows the panel. + * + * @returns {!angular.$q.Promise} A promise that is resolved when the panel is + * opened. + */ + +/** + * @ngdoc method + * @name MdPanelRef#close + * @description + * Hides and detaches the panel. Note that this will **not** destroy the panel. + * If you don't intend on using the panel again, call the {@link #destroy + * destroy} method afterwards. + * + * @returns {!angular.$q.Promise} A promise that is resolved when the panel is + * closed. + */ + +/** + * @ngdoc method + * @name MdPanelRef#attach + * @description + * Create the panel elements and attach them to the DOM. The panel will be + * hidden by default. + * + * @returns {!angular.$q.Promise} A promise that is resolved when the panel is + * attached. + */ + +/** + * @ngdoc method + * @name MdPanelRef#detach + * @description + * Removes the panel from the DOM. This will NOT hide the panel before removing + * it. + * + * @returns {!angular.$q.Promise} A promise that is resolved when the panel is + * detached. + */ + +/** + * @ngdoc method + * @name MdPanelRef#show + * @description + * Shows the panel. + * + * @returns {!angular.$q.Promise} A promise that is resolved when the panel has + * shown and animations are completed. + */ + +/** + * @ngdoc method + * @name MdPanelRef#hide + * @description + * Hides the panel. + * + * @returns {!angular.$q.Promise} A promise that is resolved when the panel has + * hidden and animations are completed. + */ + +/** + * @ngdoc method + * @name MdPanelRef#destroy + * @description + * Destroys the panel. The panel cannot be opened again after this is called. + */ + +/** + * @ngdoc method + * @name MdPanelRef#addClass + * @deprecated + * This method is in the process of being deprecated in favor of using the panel + * and container JQLite elements that are referenced in the MdPanelRef object. + * Full deprecation is scheduled for material 1.2. + * @description + * Adds a class to the panel. DO NOT use this hide/show the panel. + * + * @param {string} newClass class to be added. + * @param {boolean} toElement Whether or not to add the class to the panel + * element instead of the container. + */ + +/** + * @ngdoc method + * @name MdPanelRef#removeClass + * @deprecated + * This method is in the process of being deprecated in favor of using the panel + * and container JQLite elements that are referenced in the MdPanelRef object. + * Full deprecation is scheduled for material 1.2. + * @description + * Removes a class from the panel. DO NOT use this to hide/show the panel. + * + * @param {string} oldClass Class to be removed. + * @param {boolean} fromElement Whether or not to remove the class from the + * panel element instead of the container. + */ + +/** + * @ngdoc method + * @name MdPanelRef#toggleClass + * @deprecated + * This method is in the process of being deprecated in favor of using the panel + * and container JQLite elements that are referenced in the MdPanelRef object. + * Full deprecation is scheduled for material 1.2. + * @description + * Toggles a class on the panel. DO NOT use this to hide/show the panel. + * + * @param {string} toggleClass Class to be toggled. + * @param {boolean} onElement Whether or not to remove the class from the panel + * element instead of the container. + */ + +/** + * @ngdoc method + * @name MdPanelRef#updatePosition + * @description + * Updates the position configuration of a panel. Use this to update the + * position of a panel that is open, without having to close and re-open the + * panel. + * + * @param {!MdPanelPosition} position + */ + + +/***************************************************************************** + * MdPanelPosition * + *****************************************************************************/ + + +/** + * @ngdoc type + * @name MdPanelPosition + * @module material.components.panel + * @description + * + * Object for configuring the position of the panel. + * + * @usage + * + * #### Centering the panel + * + * + * new MdPanelPosition().absolute().center(); + * + * + * #### Overlapping the panel with an element + * + * + * new MdPanelPosition() + * .relativeTo(someElement) + * .addPanelPosition( + * $mdPanel.xPosition.ALIGN_START, + * $mdPanel.yPosition.ALIGN_TOPS + * ); + * + * + * #### Aligning the panel with the bottom of an element + * + * + * new MdPanelPosition() + * .relativeTo(someElement) + * .addPanelPosition($mdPanel.xPosition.CENTER, $mdPanel.yPosition.BELOW); + * + */ + +/** + * @ngdoc method + * @name MdPanelPosition#absolute + * @description + * Positions the panel absolutely relative to the parent element. If the parent + * is document.body, this is equivalent to positioning the panel absolutely + * within the viewport. + * + * @returns {!MdPanelPosition} + */ + +/** + * @ngdoc method + * @name MdPanelPosition#relativeTo + * @description + * Positions the panel relative to a specific element. + * + * @param {string|!Element|!angular.JQLite} element Query selector, DOM element, + * or angular element to position the panel with respect to. + * @returns {!MdPanelPosition} + */ + +/** + * @ngdoc method + * @name MdPanelPosition#top + * @description + * Sets the value of `top` for the panel. Clears any previously set vertical + * position. + * + * @param {string=} top Value of `top`. Defaults to '0'. + * @returns {!MdPanelPosition} + */ + +/** + * @ngdoc method + * @name MdPanelPosition#bottom + * @description + * Sets the value of `bottom` for the panel. Clears any previously set vertical + * position. + * + * @param {string=} bottom Value of `bottom`. Defaults to '0'. + * @returns {!MdPanelPosition} + */ + +/** + * @ngdoc method + * @name MdPanelPosition#start + * @description + * Sets the panel to the start of the page - `left` if `ltr` or `right` for + * `rtl`. Clears any previously set horizontal position. + * + * @param {string=} start Value of position. Defaults to '0'. + * @returns {!MdPanelPosition} + */ + +/** + * @ngdoc method + * @name MdPanelPosition#end + * @description + * Sets the panel to the end of the page - `right` if `ltr` or `left` for `rtl`. + * Clears any previously set horizontal position. + * + * @param {string=} end Value of position. Defaults to '0'. + * @returns {!MdPanelPosition} + */ + +/** + * @ngdoc method + * @name MdPanelPosition#left + * @description + * Sets the value of `left` for the panel. Clears any previously set + * horizontal position. + * + * @param {string=} left Value of `left`. Defaults to '0'. + * @returns {!MdPanelPosition} + */ + +/** + * @ngdoc method + * @name MdPanelPosition#right + * @description + * Sets the value of `right` for the panel. Clears any previously set + * horizontal position. + * + * @param {string=} right Value of `right`. Defaults to '0'. + * @returns {!MdPanelPosition} + */ + +/** + * @ngdoc method + * @name MdPanelPosition#centerHorizontally + * @description + * Centers the panel horizontally in the viewport. Clears any previously set + * horizontal position. + * + * @returns {!MdPanelPosition} + */ + +/** + * @ngdoc method + * @name MdPanelPosition#centerVertically + * @description + * Centers the panel vertically in the viewport. Clears any previously set + * vertical position. + * + * @returns {!MdPanelPosition} + */ + +/** + * @ngdoc method + * @name MdPanelPosition#center + * @description + * Centers the panel horizontally and vertically in the viewport. This is + * equivalent to calling both `centerHorizontally` and `centerVertically`. + * Clears any previously set horizontal and vertical positions. + * + * @returns {!MdPanelPosition} + */ + +/** + * @ngdoc method + * @name MdPanelPosition#addPanelPosition + * @description + * Sets the x and y position for the panel relative to another element. Can be + * called multiple times to specify an ordered list of panel positions. The + * first position which allows the panel to be completely on-screen will be + * chosen; the last position will be chose whether it is on-screen or not. + * + * xPosition must be one of the following values available on + * $mdPanel.xPosition: + * + * CENTER | ALIGN_START | ALIGN_END | OFFSET_START | OFFSET_END + * + * ************* + * * * + * * PANEL * + * * * + * ************* + * A B C D E + * + * A: OFFSET_START (for LTR displays) + * B: ALIGN_START (for LTR displays) + * C: CENTER + * D: ALIGN_END (for LTR displays) + * E: OFFSET_END (for LTR displays) + * + * yPosition must be one of the following values available on + * $mdPanel.yPosition: + * + * CENTER | ALIGN_TOPS | ALIGN_BOTTOMS | ABOVE | BELOW + * + * F + * G ************* + * * * + * H * PANEL * + * * * + * I ************* + * J + * + * F: BELOW + * G: ALIGN_TOPS + * H: CENTER + * I: ALIGN_BOTTOMS + * J: ABOVE + * + * @param {string} xPosition + * @param {string} yPosition + * @returns {!MdPanelPosition} + */ + +/** + * @ngdoc method + * @name MdPanelPosition#withOffsetX + * @description + * Sets the value of the offset in the x-direction. + * + * @param {string} offsetX + * @returns {!MdPanelPosition} + */ + +/** + * @ngdoc method + * @name MdPanelPosition#withOffsetY + * @description + * Sets the value of the offset in the y-direction. + * + * @param {string} offsetY + * @returns {!MdPanelPosition} + */ + + +/***************************************************************************** + * MdPanelAnimation * + *****************************************************************************/ + + +/** + * @ngdoc object + * @name MdPanelAnimation + * @description + * Animation configuration object. To use, create an MdPanelAnimation with the + * desired properties, then pass the object as part of $mdPanel creation. + * + * Example: + * + * var panelAnimation = new MdPanelAnimation() + * .openFrom(myButtonEl) + * .closeTo('.my-button') + * .withAnimation($mdPanel.animation.SCALE); + * + * $mdPanel.create({ + * animation: panelAnimation + * }); + */ + +/** + * @ngdoc method + * @name MdPanelAnimation#openFrom + * @description + * Specifies where to start the open animation. `openFrom` accepts a + * click event object, query selector, DOM element, or a Rect object that + * is used to determine the bounds. When passed a click event, the location + * of the click will be used as the position to start the animation. + * + * @param {string|!Element|!Event|{top: number, left: number}} + * @returns {!MdPanelAnimation} + */ + +/** + * @ngdoc method + * @name MdPanelAnimation#closeTo + * @description + * Specifies where to animate the panel close. `closeTo` accepts a + * query selector, DOM element, or a Rect object that is used to determine + * the bounds. + * + * @param {string|!Element|{top: number, left: number}} + * @returns {!MdPanelAnimation} + */ + +/** + * @ngdoc method + * @name MdPanelAnimation#withAnimation + * @description + * Specifies the animation class. + * + * There are several default animations that can be used: + * ($mdPanel.animation) + * SLIDE: The panel slides in and out from the specified + * elements. It will not fade in or out. + * SCALE: The panel scales in and out. Slide and fade are + * included in this animation. + * FADE: The panel fades in and out. + * + * Custom classes will by default fade in and out unless + * "transition: opacity 1ms" is added to the to custom class. + * + * @param {string|{open: string, close: string}} cssClass + * @returns {!MdPanelAnimation} + */ + + +/***************************************************************************** + * IMPLEMENTATION * + *****************************************************************************/ + + +// Default z-index for the panel. +var defaultZIndex = 80; +var MD_PANEL_HIDDEN = '_md-panel-hidden'; + +var FOCUS_TRAP_TEMPLATE = angular.element( + '
      '); + + +/** + * A service that is used for controlling/displaying panels on the screen. + * @param {!angular.JQLite} $rootElement + * @param {!angular.Scope} $rootScope + * @param {!angular.$injector} $injector + * @param {!angular.$window} $window + * @final @constructor @ngInject + */ +function MdPanelService($rootElement, $rootScope, $injector, $window) { + /** + * Default config options for the panel. + * Anything angular related needs to be done later. Therefore + * scope: $rootScope.$new(true), + * attachTo: $rootElement, + * are added later. + * @private {!Object} + */ + this._defaultConfigOptions = { + bindToController: true, + clickOutsideToClose: false, + disableParentScroll: false, + escapeToClose: false, + focusOnOpen: true, + fullscreen: false, + hasBackdrop: false, + propagateContainerEvents: false, + transformTemplate: angular.bind(this, this._wrapTemplate), + trapFocus: false, + zIndex: defaultZIndex + }; + + /** @private {!Object} */ + this._config = {}; + + /** @private @const */ + this._$rootElement = $rootElement; + + /** @private @const */ + this._$rootScope = $rootScope; + + /** @private @const */ + this._$injector = $injector; + + /** @private @const */ + this._$window = $window; + + /** @private {!Object} */ + this._trackedPanels = {}; + + /** + * Default animations that can be used within the panel. + * @type {enum} + */ + this.animation = MdPanelAnimation.animation; + + /** + * Possible values of xPosition for positioning the panel relative to + * another element. + * @type {enum} + */ + this.xPosition = MdPanelPosition.xPosition; + + /** + * Possible values of yPosition for positioning the panel relative to + * another element. + * @type {enum} + */ + this.yPosition = MdPanelPosition.yPosition; +} + + +/** + * Creates a panel with the specified options. + * @param {!Object=} config Configuration object for the panel. + * @returns {!MdPanelRef} + */ +MdPanelService.prototype.create = function(config) { + config = config || {}; + + // If the passed-in config contains an ID and the ID is within _trackedPanels, + // return the tracked panel. + if (angular.isDefined(config.id) && this._trackedPanels[config.id]) { + return this._trackedPanels[config.id]; + } + + // If no ID is set within the passed-in config, then create an arbitrary ID. + this._config = { + id: config.id || 'panel_' + this._$injector.get('$mdUtil').nextUid(), + scope: this._$rootScope.$new(true), + attachTo: this._$rootElement + }; + angular.extend(this._config, this._defaultConfigOptions, config); + + var panelRef = new MdPanelRef(this._config, this._$injector); + this._trackedPanels[config.id] = panelRef; + + return panelRef; +}; + + +/** + * Creates and opens a panel with the specified options. + * @param {!Object=} config Configuration object for the panel. + * @returns {!angular.$q.Promise} The panel created from create. + */ +MdPanelService.prototype.open = function(config) { + var panelRef = this.create(config); + return panelRef.open().then(function() { + return panelRef; + }); +}; + + +/** + * Returns a new instance of the MdPanelPosition. Use this to create the + * positioning object. + * @returns {!MdPanelPosition} + */ +MdPanelService.prototype.newPanelPosition = function() { + return new MdPanelPosition(this._$injector); +}; + + +/** + * Returns a new instance of the MdPanelAnimation. Use this to create the + * animation object. + * @returns {!MdPanelAnimation} + */ +MdPanelService.prototype.newPanelAnimation = function() { + return new MdPanelAnimation(this._$injector); +}; + + +/** + * Wraps the users template in two elements, md-panel-outer-wrapper, which + * covers the entire attachTo element, and md-panel, which contains only the + * template. This allows the panel control over positioning, animations, + * and similar properties. + * @param {string} origTemplate The original template. + * @returns {string} The wrapped template. + * @private + */ +MdPanelService.prototype._wrapTemplate = function(origTemplate) { + var template = origTemplate || ''; + + // The panel should be initially rendered offscreen so we can calculate + // height and width for positioning. + return '' + + '
      ' + + '
      ' + template + '
      ' + + '
      '; +}; + + +/***************************************************************************** + * MdPanelRef * + *****************************************************************************/ + + +/** + * A reference to a created panel. This reference contains a unique id for the + * panel, along with properties/functions used to control the panel. + * @param {!Object} config + * @param {!angular.$injector} $injector + * @final @constructor + */ +function MdPanelRef(config, $injector) { + // Injected variables. + /** @private @const {!angular.$q} */ + this._$q = $injector.get('$q'); + + /** @private @const {!angular.$mdCompiler} */ + this._$mdCompiler = $injector.get('$mdCompiler'); + + /** @private @const {!angular.$mdConstant} */ + this._$mdConstant = $injector.get('$mdConstant'); + + /** @private @const {!angular.$mdUtil} */ + this._$mdUtil = $injector.get('$mdUtil'); + + /** @private @const {!angular.Scope} */ + this._$rootScope = $injector.get('$rootScope'); + + /** @private @const {!angular.$animate} */ + this._$animate = $injector.get('$animate'); + + /** @private @const {!MdPanelRef} */ + this._$mdPanel = $injector.get('$mdPanel'); + + /** @private @const {!angular.$log} */ + this._$log = $injector.get('$log'); + + /** @private @const {!angular.$window} */ + this._$window = $injector.get('$window'); + + /** @private @const {!Function} */ + this._$$rAF = $injector.get('$$rAF'); + + // Public variables. + /** + * Unique id for the panelRef. + * @type {string} + */ + this.id = config.id; + + /** @type {!Object} */ + this.config = config; + + /** @type {!angular.JQLite|undefined} */ + this.panelContainer; + + /** @type {!angular.JQLite|undefined} */ + this.panelEl; + + /** + * Whether the panel is attached. This is synchronous. When attach is called, + * isAttached is set to true. When detach is called, isAttached is set to + * false. + * @type {boolean} + */ + this.isAttached = false; + + // Private variables. + /** @private {Array} */ + this._removeListeners = []; + + /** @private {!angular.JQLite|undefined} */ + this._topFocusTrap; + + /** @private {!angular.JQLite|undefined} */ + this._bottomFocusTrap; + + /** @private {!$mdPanel|undefined} */ + this._backdropRef; + + /** @private {Function?} */ + this._restoreScroll = null; +} + + +/** + * Opens an already created and configured panel. If the panel is already + * visible, does nothing. + * @returns {!angular.$q.Promise} A promise that is resolved when + * the panel is opened and animations finish. + */ +MdPanelRef.prototype.open = function() { + var self = this; + return this._$q(function(resolve, reject) { + var done = self._done(resolve, self); + var show = self._simpleBind(self.show, self); + + self.attach() + .then(show) + .then(done) + .catch(reject); + }); +}; + + +/** + * Closes the panel. + * @returns {!angular.$q.Promise} A promise that is resolved when + * the panel is closed and animations finish. + */ +MdPanelRef.prototype.close = function() { + var self = this; + + return this._$q(function(resolve, reject) { + var done = self._done(resolve, self); + var detach = self._simpleBind(self.detach, self); + + self.hide() + .then(detach) + .then(done) + .catch(reject); + }); +}; + + +/** + * Attaches the panel. The panel will be hidden afterwards. + * @returns {!angular.$q.Promise} A promise that is resolved when + * the panel is attached. + */ +MdPanelRef.prototype.attach = function() { + if (this.isAttached && this.panelEl) { + return this._$q.when(this); + } + + var self = this; + return this._$q(function(resolve, reject) { + var done = self._done(resolve, self); + var onDomAdded = self.config['onDomAdded'] || angular.noop; + var addListeners = function(response) { + self.isAttached = true; + self._addEventListeners(); + return response; + }; + + self._$q.all([ + self._createBackdrop(), + self._createPanel() + .then(addListeners) + .catch(reject) + ]).then(onDomAdded) + .then(done) + .catch(reject); + }); +}; + + +/** + * Only detaches the panel. Will NOT hide the panel first. + * @returns {!angular.$q.Promise} A promise that is resolved when + * the panel is detached. + */ +MdPanelRef.prototype.detach = function() { + if (!this.isAttached) { + return this._$q.when(this); + } + + var self = this; + var onDomRemoved = self.config['onDomRemoved'] || angular.noop; + + var detachFn = function() { + self._removeEventListeners(); + + // Remove the focus traps that we added earlier for keeping focus within + // the panel. + if (self._topFocusTrap && self._topFocusTrap.parentNode) { + self._topFocusTrap.parentNode.removeChild(self._topFocusTrap); + } + + if (self._bottomFocusTrap && self._bottomFocusTrap.parentNode) { + self._bottomFocusTrap.parentNode.removeChild(self._bottomFocusTrap); + } + + self.panelContainer.remove(); + self.isAttached = false; + return self._$q.when(self); + }; + + if (this._restoreScroll) { + this._restoreScroll(); + this._restoreScroll = null; + } + + return this._$q(function(resolve, reject) { + var done = self._done(resolve, self); + + self._$q.all([ + detachFn(), + self._backdropRef ? self._backdropRef.detach() : true + ]).then(onDomRemoved) + .then(done) + .catch(reject); + }); +}; + + +/** + * Destroys the panel. The Panel cannot be opened again after this. + */ +MdPanelRef.prototype.destroy = function() { + this.config.scope.$destroy(); + this.config.locals = null; +}; + + +/** + * Shows the panel. + * @returns {!angular.$q.Promise} A promise that is resolved when + * the panel has shown and animations finish. + */ +MdPanelRef.prototype.show = function() { + if (!this.panelContainer) { + return this._$q(function(resolve, reject) { + reject('Panel does not exist yet. Call open() or attach().'); + }); + } + + if (!this.panelContainer.hasClass(MD_PANEL_HIDDEN)) { + return this._$q.when(this); + } + + var self = this; + var animatePromise = function() { + self.panelContainer.removeClass(MD_PANEL_HIDDEN); + return self._animateOpen(); + }; + + return this._$q(function(resolve, reject) { + var done = self._done(resolve, self); + var onOpenComplete = self.config['onOpenComplete'] || angular.noop; + + self._$q.all([ + self._backdropRef ? self._backdropRef.show() : self, + animatePromise().then(function() { self._focusOnOpen(); }, reject) + ]).then(onOpenComplete) + .then(done) + .catch(reject); + }); +}; + + +/** + * Hides the panel. + * @returns {!angular.$q.Promise} A promise that is resolved when + * the panel has hidden and animations finish. + */ +MdPanelRef.prototype.hide = function() { + if (!this.panelContainer) { + return this._$q(function(resolve, reject) { + reject('Panel does not exist yet. Call open() or attach().'); + }); + } + + if (this.panelContainer.hasClass(MD_PANEL_HIDDEN)) { + return this._$q.when(this); + } + + var self = this; + + return this._$q(function(resolve, reject) { + var done = self._done(resolve, self); + var onRemoving = self.config['onRemoving'] || angular.noop; + + var focusOnOrigin = function() { + var origin = self.config['origin']; + if (origin) { + getElement(origin).focus(); + } + }; + + var hidePanel = function() { + self.panelContainer.addClass(MD_PANEL_HIDDEN); + }; + + self._$q.all([ + self._backdropRef ? self._backdropRef.hide() : self, + self._animateClose() + .then(onRemoving) + .then(hidePanel) + .then(focusOnOrigin) + .catch(reject) + ]).then(done, reject); + }); +}; + + +/** + * Add a class to the panel. DO NOT use this to hide/show the panel. + * @deprecated + * This method is in the process of being deprecated in favor of using the panel + * and container JQLite elements that are referenced in the MdPanelRef object. + * Full deprecation is scheduled for material 1.2. + * + * @param {string} newClass Class to be added. + * @param {boolean} toElement Whether or not to add the class to the panel + * element instead of the container. + */ +MdPanelRef.prototype.addClass = function(newClass, toElement) { + this._$log.warn( + 'The addClass method is in the process of being deprecated. ' + + 'Full deprecation is scheduled for the Angular Material 1.2 release. ' + + 'To achieve the same results, use the panelContainer or panelEl ' + + 'JQLite elements that are referenced in MdPanelRef.'); + + if (!this.panelContainer) { + throw new Error('Panel does not exist yet. Call open() or attach().'); + } + + if (!toElement && !this.panelContainer.hasClass(newClass)) { + this.panelContainer.addClass(newClass); + } else if (toElement && !this.panelEl.hasClass(newClass)) { + this.panelEl.addClass(newClass); + } +}; + + +/** + * Remove a class from the panel. DO NOT use this to hide/show the panel. + * @deprecated + * This method is in the process of being deprecated in favor of using the panel + * and container JQLite elements that are referenced in the MdPanelRef object. + * Full deprecation is scheduled for material 1.2. + * + * @param {string} oldClass Class to be removed. + * @param {boolean} fromElement Whether or not to remove the class from the + * panel element instead of the container. + */ +MdPanelRef.prototype.removeClass = function(oldClass, fromElement) { + this._$log.warn( + 'The removeClass method is in the process of being deprecated. ' + + 'Full deprecation is scheduled for the Angular Material 1.2 release. ' + + 'To achieve the same results, use the panelContainer or panelEl ' + + 'JQLite elements that are referenced in MdPanelRef.'); + + if (!this.panelContainer) { + throw new Error('Panel does not exist yet. Call open() or attach().'); + } + + if (!fromElement && this.panelContainer.hasClass(oldClass)) { + this.panelContainer.removeClass(oldClass); + } else if (fromElement && this.panelEl.hasClass(oldClass)) { + this.panelEl.removeClass(oldClass); + } +}; + + +/** + * Toggle a class on the panel. DO NOT use this to hide/show the panel. + * @deprecated + * This method is in the process of being deprecated in favor of using the panel + * and container JQLite elements that are referenced in the MdPanelRef object. + * Full deprecation is scheduled for material 1.2. + * + * @param {string} toggleClass The class to toggle. + * @param {boolean} onElement Whether or not to toggle the class on the panel + * element instead of the container. + */ +MdPanelRef.prototype.toggleClass = function(toggleClass, onElement) { + this._$log.warn( + 'The toggleClass method is in the process of being deprecated. ' + + 'Full deprecation is scheduled for the Angular Material 1.2 release. ' + + 'To achieve the same results, use the panelContainer or panelEl ' + + 'JQLite elements that are referenced in MdPanelRef.'); + + if (!this.panelContainer) { + throw new Error('Panel does not exist yet. Call open() or attach().'); + } + + if (!onElement) { + this.panelContainer.toggleClass(toggleClass); + } else { + this.panelEl.toggleClass(toggleClass); + } +}; + + +/** + * Creates a panel and adds it to the dom. + * @returns {!angular.$q.Promise} A promise that is resolved when the panel is + * created. + * @private + */ +MdPanelRef.prototype._createPanel = function() { + var self = this; + + return this._$q(function(resolve, reject) { + if (!self.config.locals) { + self.config.locals = {}; + } + + self.config.locals.mdPanelRef = self; + self._$mdCompiler.compile(self.config) + .then(function(compileData) { + self.panelContainer = compileData.link(self.config['scope']); + getElement(self.config['attachTo']).append(self.panelContainer); + + if (self.config['disableParentScroll']) { + self._restoreScroll = self._$mdUtil.disableScrollAround( + null, + self.panelContainer, + { disableScrollMask: true } + ); + } + + self.panelEl = angular.element( + self.panelContainer[0].querySelector('.md-panel')); + + // Add a custom CSS class to the panel element. + if (self.config['panelClass']) { + self.panelEl.addClass(self.config['panelClass']); + } + + // Handle click and touch events for the panel container. + if (self.config['propagateContainerEvents']) { + self.panelContainer.css('pointer-events', 'none'); + } + + // Panel may be outside the $rootElement, tell ngAnimate to animate + // regardless. + if (self._$animate.pin) { + self._$animate.pin(self.panelContainer, + getElement(self.config['attachTo'])); + } + + self._configureTrapFocus(); + self._addStyles().then(function() { + resolve(self); + }, reject); + }, reject); + }); +}; + + +/** + * Adds the styles for the panel, such as positioning and z-index. + * @returns {!angular.$q.Promise} + * @private + */ +MdPanelRef.prototype._addStyles = function() { + var self = this; + return this._$q(function(resolve) { + self.panelContainer.css('z-index', self.config['zIndex']); + self.panelEl.css('z-index', self.config['zIndex'] + 1); + + var hideAndResolve = function() { + // Remove left: -9999px and add hidden class. + self.panelEl.css('left', ''); + self.panelContainer.addClass(MD_PANEL_HIDDEN); + resolve(self); + }; + + if (self.config['fullscreen']) { + self.panelEl.addClass('_md-panel-fullscreen'); + hideAndResolve(); + return; // Don't setup positioning. + } + + var positionConfig = self.config['position']; + if (!positionConfig) { + hideAndResolve(); + return; // Don't setup positioning. + } + + // Wait for angular to finish processing the template, then position it + // correctly. This is necessary so that the panel will have a defined height + // and width. + self._$rootScope['$$postDigest'](function() { + self._updatePosition(true); + resolve(self); + }); + }); +}; + + +/** + * Updates the position configuration of a panel + * @param {!MdPanelPosition} position + */ +MdPanelRef.prototype.updatePosition = function(position) { + if (!this.panelContainer) { + throw new Error('Panel does not exist yet. Call open() or attach().'); + } + + this.config['position'] = position; + this._updatePosition(); +}; + + +/** + * Calculates and updates the position of the panel. + * @param {boolean=} init + * @private + */ +MdPanelRef.prototype._updatePosition = function(init) { + var positionConfig = this.config['position']; + + if (positionConfig) { + positionConfig._setPanelPosition(this.panelEl); + + // Hide the panel now that position is known. + if (init) { + this.panelContainer.addClass(MD_PANEL_HIDDEN); + } + + this.panelEl.css( + MdPanelPosition.absPosition.TOP, + positionConfig.getTop() + ); + this.panelEl.css( + MdPanelPosition.absPosition.BOTTOM, + positionConfig.getBottom() + ); + this.panelEl.css( + MdPanelPosition.absPosition.LEFT, + positionConfig.getLeft() + ); + this.panelEl.css( + MdPanelPosition.absPosition.RIGHT, + positionConfig.getRight() + ); + + // Use the vendor prefixed version of transform. + var prefixedTransform = this._$mdConstant.CSS.TRANSFORM; + this.panelEl.css(prefixedTransform, positionConfig.getTransform()); + } +}; + + +/** + * Focuses on the panel or the first focus target. + * @private + */ +MdPanelRef.prototype._focusOnOpen = function() { + if (this.config['focusOnOpen']) { + // Wait for the template to finish rendering to guarantee md-autofocus has + // finished adding the class md-autofocus, otherwise the focusable element + // isn't available to focus. + var self = this; + this._$rootScope['$$postDigest'](function() { + var target = self._$mdUtil.findFocusTarget(self.panelEl) || + self.panelEl; + target.focus(); + }); + } +}; + + +/** + * Shows the backdrop. + * @returns {!angular.$q.Promise} A promise that is resolved when the backdrop + * is created and attached. + * @private + */ +MdPanelRef.prototype._createBackdrop = function() { + if (this.config.hasBackdrop) { + if (!this._backdropRef) { + var backdropAnimation = this._$mdPanel.newPanelAnimation() + .openFrom(this.config.attachTo) + .withAnimation({ + open: '_md-opaque-enter', + close: '_md-opaque-leave' + }); + var backdropConfig = { + animation: backdropAnimation, + attachTo: this.config.attachTo, + focusOnOpen: false, + panelClass: '_md-panel-backdrop', + zIndex: this.config.zIndex - 1 + }; + this._backdropRef = this._$mdPanel.create(backdropConfig); + } + if (!this._backdropRef.isAttached) { + return this._backdropRef.attach(); + } + } +}; + + +/** + * Listen for escape keys and outside clicks to auto close. + * @private + */ +MdPanelRef.prototype._addEventListeners = function() { + this._configureEscapeToClose(); + this._configureClickOutsideToClose(); + this._configureScrollListener(); +}; + + +/** + * Remove event listeners added in _addEventListeners. + * @private + */ +MdPanelRef.prototype._removeEventListeners = function() { + this._removeListeners && this._removeListeners.forEach(function(removeFn) { + removeFn(); + }); + this._removeListeners = []; +}; + + +/** + * Setup the escapeToClose event listeners. + * @private + */ +MdPanelRef.prototype._configureEscapeToClose = function() { + if (this.config['escapeToClose']) { + var parentTarget = getElement(this.config['attachTo']); + var self = this; + + var keyHandlerFn = function(ev) { + if (ev.keyCode === self._$mdConstant.KEY_CODE.ESCAPE) { + ev.stopPropagation(); + ev.preventDefault(); + + self.close(); + } + }; + + // Add keydown listeners + this.panelContainer.on('keydown', keyHandlerFn); + parentTarget.on('keydown', keyHandlerFn); + + // Queue remove listeners function + this._removeListeners.push(function() { + self.panelContainer.off('keydown', keyHandlerFn); + parentTarget.off('keydown', keyHandlerFn); + }); + } +}; + + +/** + * Setup the clickOutsideToClose event listeners. + * @private + */ +MdPanelRef.prototype._configureClickOutsideToClose = function() { + if (this.config['clickOutsideToClose']) { + var target = this.panelContainer; + var sourceElem; + + // Keep track of the element on which the mouse originally went down + // so that we can only close the backdrop when the 'click' started on it. + // A simple 'click' handler does not work, + // it sets the target object as the element the mouse went down on. + var mousedownHandler = function(ev) { + sourceElem = ev.target; + }; + + // We check if our original element and the target is the backdrop + // because if the original was the backdrop and the target was inside the + // panel we don't want to panel to close. + var self = this; + var mouseupHandler = function(ev) { + if (sourceElem === target[0] && ev.target === target[0]) { + ev.stopPropagation(); + ev.preventDefault(); + + self.close(); + } + }; + + // Add listeners + target.on('mousedown', mousedownHandler); + target.on('mouseup', mouseupHandler); + + // Queue remove listeners function + this._removeListeners.push(function() { + target.off('mousedown', mousedownHandler); + target.off('mouseup', mouseupHandler); + }); + } +}; + + +/** + * Configures the listeners for updating the panel position on scroll. + * @private +*/ +MdPanelRef.prototype._configureScrollListener = function() { + var updatePosition = angular.bind(this, this._updatePosition); + var debouncedUpdatePosition = this._$$rAF.throttle(updatePosition); + var self = this; + + var onScroll = function() { + if (!self.config['disableParentScroll']) { + debouncedUpdatePosition(); + } + }; + + // Add listeners. + this._$window.addEventListener('scroll', onScroll, true); + + // Queue remove listeners function. + this._removeListeners.push(function() { + self._$window.removeEventListener('scroll', onScroll, true); + }); +}; + + +/** + * Setup the focus traps. These traps will wrap focus when tabbing past the + * panel. When shift-tabbing, the focus will stick in place. + * @private + */ +MdPanelRef.prototype._configureTrapFocus = function() { + // Focus doesn't remain instead of the panel without this. + this.panelEl.attr('tabIndex', '-1'); + if (this.config['trapFocus']) { + var element = this.panelEl; + // Set up elements before and after the panel to capture focus and + // redirect back into the panel. + this._topFocusTrap = FOCUS_TRAP_TEMPLATE.clone()[0]; + this._bottomFocusTrap = FOCUS_TRAP_TEMPLATE.clone()[0]; + + // When focus is about to move out of the panel, we want to intercept it + // and redirect it back to the panel element. + var focusHandler = function() { + element.focus(); + }; + this._topFocusTrap.addEventListener('focus', focusHandler); + this._bottomFocusTrap.addEventListener('focus', focusHandler); + + // Queue remove listeners function + this._removeListeners.push(this._simpleBind(function() { + this._topFocusTrap.removeEventListener('focus', focusHandler); + this._bottomFocusTrap.removeEventListener('focus', focusHandler); + }, this)); + + // The top focus trap inserted immediately before the md-panel element (as + // a sibling). The bottom focus trap inserted immediately after the + // md-panel element (as a sibling). + element[0].parentNode.insertBefore(this._topFocusTrap, element[0]); + element.after(this._bottomFocusTrap); + } +}; + + +/** + * Animate the panel opening. + * @returns {!angular.$q.Promise} A promise that is resolved when the panel has + * animated open. + * @private + */ +MdPanelRef.prototype._animateOpen = function() { + this.panelContainer.addClass('md-panel-is-showing'); + var animationConfig = this.config['animation']; + if (!animationConfig) { + // Promise is in progress, return it. + this.panelContainer.addClass('_md-panel-shown'); + return this._$q.when(this); + } + + var self = this; + return this._$q(function(resolve) { + var done = self._done(resolve, self); + var warnAndOpen = function() { + self._$log.warn( + 'MdPanel Animations failed. Showing panel without animating.'); + done(); + }; + + animationConfig.animateOpen(self.panelEl) + .then(done, warnAndOpen); + }); +}; + + +/** + * Animate the panel closing. + * @returns {!angular.$q.Promise} A promise that is resolved when the panel has + * animated closed. + * @private + */ +MdPanelRef.prototype._animateClose = function() { + var animationConfig = this.config['animation']; + if (!animationConfig) { + this.panelContainer.removeClass('md-panel-is-showing'); + this.panelContainer.removeClass('_md-panel-shown'); + return this._$q.when(this); + } + + var self = this; + return this._$q(function(resolve) { + var done = function() { + self.panelContainer.removeClass('md-panel-is-showing'); + resolve(self); + }; + var warnAndClose = function() { + self._$log.warn( + 'MdPanel Animations failed. Hiding panel without animating.'); + done(); + }; + + animationConfig.animateClose(self.panelEl) + .then(done, warnAndClose); + }); +}; + + +/** + * Faster, more basic than angular.bind + * http://jsperf.com/angular-bind-vs-custom-vs-native + * @param {function} callback + * @param {!Object} self + * @return {function} Callback function with a bound self. + */ +MdPanelRef.prototype._simpleBind = function(callback, self) { + return function(value) { + return callback.apply(self, value); + }; +}; + + +/** + * @param {function} callback + * @param {!Object} self + * @return {function} Callback function with a self param. + */ +MdPanelRef.prototype._done = function(callback, self) { + return function() { + callback(self); + }; +}; + + +/***************************************************************************** + * MdPanelPosition * + *****************************************************************************/ + + +/** + * Position configuration object. To use, create an MdPanelPosition with the + * desired properties, then pass the object as part of $mdPanel creation. + * + * Example: + * + * var panelPosition = new MdPanelPosition() + * .relativeTo(myButtonEl) + * .addPanelPosition( + * $mdPanel.xPosition.CENTER, + * $mdPanel.yPosition.ALIGN_TOPS + * ); + * + * $mdPanel.create({ + * position: panelPosition + * }); + * + * @param {!angular.$injector} $injector + * @final @constructor + */ +function MdPanelPosition($injector) { + /** @private @const {!angular.$window} */ + this._$window = $injector.get('$window'); + + /** @private {boolean} */ + this._isRTL = $injector.get('$mdUtil').bidi() === 'rtl'; + + /** @private {boolean} */ + this._absolute = false; + + /** @private {!angular.JQLite} */ + this._relativeToEl; + + /** @private {string} */ + this._top = ''; + + /** @private {string} */ + this._bottom = ''; + + /** @private {string} */ + this._left = ''; + + /** @private {string} */ + this._right = ''; + + /** @private {!Array} */ + this._translateX = []; + + /** @private {!Array} */ + this._translateY = []; + + /** @private {!Array<{x:string, y:string}>} */ + this._positions = []; + + /** @private {?{x:string, y:string}} */ + this._actualPosition; +} + + +/** + * Possible values of xPosition. + * @enum {string} + */ +MdPanelPosition.xPosition = { + CENTER: 'center', + ALIGN_START: 'align-start', + ALIGN_END: 'align-end', + OFFSET_START: 'offset-start', + OFFSET_END: 'offset-end' +}; + + +/** + * Possible values of yPosition. + * @enum {string} + */ +MdPanelPosition.yPosition = { + CENTER: 'center', + ALIGN_TOPS: 'align-tops', + ALIGN_BOTTOMS: 'align-bottoms', + ABOVE: 'above', + BELOW: 'below' +}; + + +/** + * Possible values of absolute position. + * @enum {string} + */ +MdPanelPosition.absPosition = { + TOP: 'top', + RIGHT: 'right', + BOTTOM: 'bottom', + LEFT: 'left' +}; + + +/** + * Sets absolute positioning for the panel. + * @return {!MdPanelPosition} + */ +MdPanelPosition.prototype.absolute = function() { + this._absolute = true; + return this; +}; + +/** + * Sets the value of a position for the panel. Clears any previously set + * position. + * @param {string} position Position to set + * @param {string=} value Value of the position. Defaults to '0'. + * @returns {!MdPanelPosition} + * @private + */ +MdPanelPosition.prototype._setPosition = function(position, value) { + if (position === MdPanelPosition.absPosition.RIGHT || + position === MdPanelPosition.absPosition.LEFT) { + this._left = this._right = ''; + } else if ( + position === MdPanelPosition.absPosition.BOTTOM || + position === MdPanelPosition.absPosition.TOP) { + this._top = this._bottom = ''; + } else { + var positions = Object.keys(MdPanelPosition.absPosition).join() + .toLowerCase(); + + throw new Error('Position must be one of ' + positions + '.'); + } + + this['_' + position] = angular.isString(value) ? value : '0'; + + return this; +}; + + +/** + * Sets the value of `top` for the panel. Clears any previously set vertical + * position. + * @param {string=} top Value of `top`. Defaults to '0'. + * @returns {!MdPanelPosition} + */ +MdPanelPosition.prototype.top = function(top) { + return this._setPosition(MdPanelPosition.absPosition.TOP, top); +}; + + +/** + * Sets the value of `bottom` for the panel. Clears any previously set vertical + * position. + * @param {string=} bottom Value of `bottom`. Defaults to '0'. + * @returns {!MdPanelPosition} + */ +MdPanelPosition.prototype.bottom = function(bottom) { + return this._setPosition(MdPanelPosition.absPosition.BOTTOM, bottom); +}; + + +/** + * Sets the panel to the start of the page - `left` if `ltr` or `right` for + * `rtl`. Clears any previously set horizontal position. + * @param {string=} start Value of position. Defaults to '0'. + * @returns {!MdPanelPosition} + */ +MdPanelPosition.prototype.start = function(start) { + var position = this._isRTL ? MdPanelPosition.absPosition.RIGHT : MdPanelPosition.absPosition.LEFT; + return this._setPosition(position, start); +}; + + +/** + * Sets the panel to the end of the page - `right` if `ltr` or `left` for `rtl`. + * Clears any previously set horizontal position. + * @param {string=} end Value of position. Defaults to '0'. + * @returns {!MdPanelPosition} + */ +MdPanelPosition.prototype.end = function(end) { + var position = this._isRTL ? MdPanelPosition.absPosition.LEFT : MdPanelPosition.absPosition.RIGHT; + return this._setPosition(position, end); +}; + + +/** + * Sets the value of `left` for the panel. Clears any previously set + * horizontal position. + * @param {string=} left Value of `left`. Defaults to '0'. + * @returns {!MdPanelPosition} + */ +MdPanelPosition.prototype.left = function(left) { + return this._setPosition(MdPanelPosition.absPosition.LEFT, left); +}; + + +/** + * Sets the value of `right` for the panel. Clears any previously set + * horizontal position. + * @param {string=} right Value of `right`. Defaults to '0'. + * @returns {!MdPanelPosition} +*/ +MdPanelPosition.prototype.right = function(right) { + return this._setPosition(MdPanelPosition.absPosition.RIGHT, right); +}; + + +/** + * Centers the panel horizontally in the viewport. Clears any previously set + * horizontal position. + * @returns {!MdPanelPosition} + */ +MdPanelPosition.prototype.centerHorizontally = function() { + this._left = '50%'; + this._right = ''; + this._translateX = ['-50%']; + return this; +}; + + +/** + * Centers the panel vertically in the viewport. Clears any previously set + * vertical position. + * @returns {!MdPanelPosition} + */ +MdPanelPosition.prototype.centerVertically = function() { + this._top = '50%'; + this._bottom = ''; + this._translateY = ['-50%']; + return this; +}; + + +/** + * Centers the panel horizontally and vertically in the viewport. This is + * equivalent to calling both `centerHorizontally` and `centerVertically`. + * Clears any previously set horizontal and vertical positions. + * @returns {!MdPanelPosition} + */ +MdPanelPosition.prototype.center = function() { + return this.centerHorizontally().centerVertically(); +}; + + +/** + * Sets element for relative positioning. + * @param {string|!Element|!angular.JQLite} element Query selector, DOM element, + * or angular element to set the panel relative to. + * @returns {!MdPanelPosition} + */ +MdPanelPosition.prototype.relativeTo = function(element) { + this._absolute = false; + this._relativeToEl = getElement(element); + return this; +}; + + +/** + * Sets the x and y positions for the panel relative to another element. + * @param {string} xPosition must be one of the MdPanelPosition.xPosition + * values. + * @param {string} yPosition must be one of the MdPanelPosition.yPosition + * values. + * @returns {!MdPanelPosition} + */ +MdPanelPosition.prototype.addPanelPosition = function(xPosition, yPosition) { + if (!this._relativeToEl) { + throw new Error('addPanelPosition can only be used with relative ' + + 'positioning. Set relativeTo first.'); + } + + this._validateXPosition(xPosition); + this._validateYPosition(yPosition); + + this._positions.push({ + x: xPosition, + y: yPosition, + }); + return this; +}; + + +/** + * Ensures that yPosition is a valid position name. Throw an exception if not. + * @param {string} yPosition + */ +MdPanelPosition.prototype._validateYPosition = function(yPosition) { + // empty is ok + if (yPosition == null) { + return; + } + + var positionKeys = Object.keys(MdPanelPosition.yPosition); + var positionValues = []; + for (var key, i = 0; key = positionKeys[i]; i++) { + var position = MdPanelPosition.yPosition[key]; + positionValues.push(position); + + if (position === yPosition) { + return; + } + } + + throw new Error('Panel y position only accepts the following values:\n' + + positionValues.join(' | ')); +}; + + +/** + * Ensures that xPosition is a valid position name. Throw an exception if not. + * @param {string} xPosition + */ +MdPanelPosition.prototype._validateXPosition = function(xPosition) { + // empty is ok + if (xPosition == null) { + return; + } + + var positionKeys = Object.keys(MdPanelPosition.xPosition); + var positionValues = []; + for (var key, i = 0; key = positionKeys[i]; i++) { + var position = MdPanelPosition.xPosition[key]; + positionValues.push(position); + if (position === xPosition) { + return; + } + } + + throw new Error('Panel x Position only accepts the following values:\n' + + positionValues.join(' | ')); +}; + + +/** + * Sets the value of the offset in the x-direction. This will add to any + * previously set offsets. + * @param {string} offsetX + * @returns {!MdPanelPosition} + */ +MdPanelPosition.prototype.withOffsetX = function(offsetX) { + this._translateX.push(offsetX); + return this; +}; + + +/** + * Sets the value of the offset in the y-direction. This will add to any + * previously set offsets. + * @param {string} offsetY + * @returns {!MdPanelPosition} + */ +MdPanelPosition.prototype.withOffsetY = function(offsetY) { + this._translateY.push(offsetY); + return this; +}; + + +/** + * Gets the value of `top` for the panel. + * @returns {string} + */ +MdPanelPosition.prototype.getTop = function() { + return this._top; +}; + + +/** + * Gets the value of `bottom` for the panel. + * @returns {string} + */ +MdPanelPosition.prototype.getBottom = function() { + return this._bottom; +}; + + +/** + * Gets the value of `left` for the panel. + * @returns {string} + */ +MdPanelPosition.prototype.getLeft = function() { + return this._left; +}; + + +/** + * Gets the value of `right` for the panel. + * @returns {string} + */ +MdPanelPosition.prototype.getRight = function() { + return this._right; +}; + + +/** + * Gets the value of `transform` for the panel. + * @returns {string} + */ +MdPanelPosition.prototype.getTransform = function() { + var translateX = this._reduceTranslateValues('translateX', this._translateX); + var translateY = this._reduceTranslateValues('translateY', this._translateY); + + // It's important to trim the result, because the browser will ignore the set + // operation if the string contains only whitespace. + return (translateX + ' ' + translateY).trim(); +}; + +/** + * True if the panel is completely on-screen with this positioning; false + * otherwise. + * @param {!angular.JQLite} panelEl + * @return {boolean} + */ +MdPanelPosition.prototype._isOnscreen = function(panelEl) { + // this works because we always use fixed positioning for the panel, + // which is relative to the viewport. + // TODO(gmoothart): take into account _translateX and _translateY to the + // extent feasible. + + var left = parseInt(this.getLeft()); + var top = parseInt(this.getTop()); + var right = left + panelEl[0].offsetWidth; + var bottom = top + panelEl[0].offsetHeight; + + return (left >= 0) && + (top >= 0) && + (bottom <= this._$window.innerHeight) && + (right <= this._$window.innerWidth); +}; + + +/** + * Gets the first x/y position that can fit on-screen. + * @returns {{x: string, y: string}} + */ +MdPanelPosition.prototype.getActualPosition = function() { + return this._actualPosition; +}; + + +/** + * Reduces a list of translate values to a string that can be used within + * transform. + * @param {string} translateFn + * @param {!Array} values + * @returns {string} + * @private + */ +MdPanelPosition.prototype._reduceTranslateValues = + function(translateFn, values) { + return values.map(function(translation) { + return translateFn + '(' + translation + ')'; + }).join(' '); + }; + + +/** + * Sets the panel position based on the created panel element and best x/y + * positioning. + * @param {!angular.JQLite} panelEl + * @private + */ +MdPanelPosition.prototype._setPanelPosition = function(panelEl) { + // Only calculate the position if necessary. + if (this._absolute) { + return; + } + + if (this._actualPosition) { + this._calculatePanelPosition(panelEl, this._actualPosition); + return; + } + + for (var i = 0; i < this._positions.length; i++) { + this._actualPosition = this._positions[i]; + this._calculatePanelPosition(panelEl, this._actualPosition); + if (this._isOnscreen(panelEl)) { + break; + } + } +}; + + +/** + * Switches between 'start' and 'end'. + * @param {string} position Horizontal position of the panel + * @returns {string} Reversed position + * @private + */ +MdPanelPosition.prototype._reverseXPosition = function(position) { + if (position === MdPanelPosition.xPosition.CENTER) { + return; + } + + var start = 'start'; + var end = 'end'; + + return position.indexOf(start) > -1 ? position.replace(start, end) : position.replace(end, start); +}; + + +/** + * Handles horizontal positioning in rtl or ltr environments. + * @param {string} position Horizontal position of the panel + * @returns {string} The correct position according the page direction + * @private + */ +MdPanelPosition.prototype._bidi = function(position) { + return this._isRTL ? this._reverseXPosition(position) : position; +}; + + +/** + * Calculates the panel position based on the created panel element and the + * provided positioning. + * @param {!angular.JQLite} panelEl + * @param {!{x:string, y:string}} position + * @private + */ +MdPanelPosition.prototype._calculatePanelPosition = function(panelEl, position) { + + var panelBounds = panelEl[0].getBoundingClientRect(); + var panelWidth = panelBounds.width; + var panelHeight = panelBounds.height; + + var targetBounds = this._relativeToEl[0].getBoundingClientRect(); + + var targetLeft = targetBounds.left; + var targetRight = targetBounds.right; + var targetWidth = targetBounds.width; + + switch (this._bidi(position.x)) { + case MdPanelPosition.xPosition.OFFSET_START: + this._left = targetLeft - panelWidth + 'px'; + break; + case MdPanelPosition.xPosition.ALIGN_END: + this._left = targetRight - panelWidth + 'px'; + break; + case MdPanelPosition.xPosition.CENTER: + var left = targetLeft + (0.5 * targetWidth) - (0.5 * panelWidth); + this._left = left + 'px'; + break; + case MdPanelPosition.xPosition.ALIGN_START: + this._left = targetLeft + 'px'; + break; + case MdPanelPosition.xPosition.OFFSET_END: + this._left = targetRight + 'px'; + break; + } + + var targetTop = targetBounds.top; + var targetBottom = targetBounds.bottom; + var targetHeight = targetBounds.height; + + switch (position.y) { + case MdPanelPosition.yPosition.ABOVE: + this._top = targetTop - panelHeight + 'px'; + break; + case MdPanelPosition.yPosition.ALIGN_BOTTOMS: + this._top = targetBottom - panelHeight + 'px'; + break; + case MdPanelPosition.yPosition.CENTER: + var top = targetTop + (0.5 * targetHeight) - (0.5 * panelHeight); + this._top = top + 'px'; + break; + case MdPanelPosition.yPosition.ALIGN_TOPS: + this._top = targetTop + 'px'; + break; + case MdPanelPosition.yPosition.BELOW: + this._top = targetBottom + 'px'; + break; + } +}; + + +/***************************************************************************** + * MdPanelAnimation * + *****************************************************************************/ + + +/** + * Animation configuration object. To use, create an MdPanelAnimation with the + * desired properties, then pass the object as part of $mdPanel creation. + * + * Example: + * + * var panelAnimation = new MdPanelAnimation() + * .openFrom(myButtonEl) + * .closeTo('.my-button') + * .withAnimation($mdPanel.animation.SCALE); + * + * $mdPanel.create({ + * animation: panelAnimation + * }); + * + * @param {!angular.$injector} $injector + * @final @constructor + */ +function MdPanelAnimation($injector) { + /** @private @const {!angular.$mdUtil} */ + this._$mdUtil = $injector.get('$mdUtil'); + + /** + * @private {{element: !angular.JQLite|undefined, bounds: !DOMRect}| + * undefined} + */ + this._openFrom; + + /** + * @private {{element: !angular.JQLite|undefined, bounds: !DOMRect}| + * undefined} + */ + this._closeTo; + + /** @private {string|{open: string, close: string}} */ + this._animationClass = ''; +} + + +/** + * Possible default animations. + * @enum {string} + */ +MdPanelAnimation.animation = { + SLIDE: 'md-panel-animate-slide', + SCALE: 'md-panel-animate-scale', + FADE: 'md-panel-animate-fade' +}; + + +/** + * Specifies where to start the open animation. `openFrom` accepts a + * click event object, query selector, DOM element, or a Rect object that + * is used to determine the bounds. When passed a click event, the location + * of the click will be used as the position to start the animation. + * @param {string|!Element|!Event|{top: number, left: number}} openFrom + * @returns {!MdPanelAnimation} + */ +MdPanelAnimation.prototype.openFrom = function(openFrom) { + // Check if 'openFrom' is an Event. + openFrom = openFrom.target ? openFrom.target : openFrom; + + this._openFrom = this._getPanelAnimationTarget(openFrom); + + if (!this._closeTo) { + this._closeTo = this._openFrom; + } + return this; +}; + + +/** + * Specifies where to animate the panel close. `closeTo` accepts a + * query selector, DOM element, or a Rect object that is used to determine + * the bounds. + * @param {string|!Element|{top: number, left: number}} closeTo + * @returns {!MdPanelAnimation} + */ +MdPanelAnimation.prototype.closeTo = function(closeTo) { + this._closeTo = this._getPanelAnimationTarget(closeTo); + return this; +}; + + +/** + * Returns the element and bounds for the animation target. + * @param {string|!Element|{top: number, left: number}} location + * @returns {{element: !angular.JQLite|undefined, bounds: !DOMRect}} + * @private + */ +MdPanelAnimation.prototype._getPanelAnimationTarget = function(location) { + if (angular.isDefined(location.top) || angular.isDefined(location.left)) { + return { + element: undefined, + bounds: { + top: location.top || 0, + left: location.left || 0 + } + }; + } else { + return this._getBoundingClientRect(getElement(location)); + } +}; + + +/** + * Specifies the animation class. + * + * There are several default animations that can be used: + * (MdPanelAnimation.animation) + * SLIDE: The panel slides in and out from the specified + * elements. + * SCALE: The panel scales in and out. + * FADE: The panel fades in and out. + * + * @param {string|{open: string, close: string}} cssClass + * @returns {!MdPanelAnimation} + */ +MdPanelAnimation.prototype.withAnimation = function(cssClass) { + this._animationClass = cssClass; + return this; +}; + + +/** + * Animate the panel open. + * @param {!angular.JQLite} panelEl + * @returns {!angular.$q.Promise} A promise that is resolved when the open + * animation is complete. + */ +MdPanelAnimation.prototype.animateOpen = function(panelEl) { + var animator = this._$mdUtil.dom.animator; + + this._fixBounds(panelEl); + var animationOptions = {}; + + // Include the panel transformations when calculating the animations. + var panelTransform = panelEl[0].style.transform || ''; + + var openFrom = animator.toTransformCss(panelTransform); + var openTo = animator.toTransformCss(panelTransform); + + switch (this._animationClass) { + case MdPanelAnimation.animation.SLIDE: + // Slide should start with opacity: 1. + panelEl.css('opacity', '1'); + + animationOptions = { + transitionInClass: '_md-panel-animate-enter' + }; + + var openSlide = animator.calculateSlideToOrigin( + panelEl, this._openFrom) || ''; + openFrom = animator.toTransformCss(openSlide + ' ' + panelTransform); + break; + + case MdPanelAnimation.animation.SCALE: + animationOptions = { + transitionInClass: '_md-panel-animate-enter' + }; + + var openScale = animator.calculateZoomToOrigin( + panelEl, this._openFrom) || ''; + openFrom = animator.toTransformCss(openScale + ' ' + panelTransform); + break; + + case MdPanelAnimation.animation.FADE: + animationOptions = { + transitionInClass: '_md-panel-animate-enter' + }; + break; + + default: + if (angular.isString(this._animationClass)) { + animationOptions = { + transitionInClass: this._animationClass + }; + } else { + animationOptions = { + transitionInClass: this._animationClass['open'], + transitionOutClass: this._animationClass['close'], + }; + } + } + + return animator + .translate3d(panelEl, openFrom, openTo, animationOptions); +}; + + +/** + * Animate the panel close. + * @param {!angular.JQLite} panelEl + * @returns {!angular.$q.Promise} A promise that resolves when the close + * animation is complete. + */ +MdPanelAnimation.prototype.animateClose = function(panelEl) { + var animator = this._$mdUtil.dom.animator; + var reverseAnimationOptions = {}; + + // Include the panel transformations when calculating the animations. + var panelTransform = panelEl[0].style.transform || ''; + + var closeFrom = animator.toTransformCss(panelTransform); + var closeTo = animator.toTransformCss(panelTransform); + + switch (this._animationClass) { + case MdPanelAnimation.animation.SLIDE: + // Slide should start with opacity: 1. + panelEl.css('opacity', '1'); + reverseAnimationOptions = { + transitionInClass: '_md-panel-animate-leave' + }; + + var closeSlide = animator.calculateSlideToOrigin( + panelEl, this._closeTo) || ''; + closeTo = animator.toTransformCss(closeSlide + ' ' + panelTransform); + break; + + case MdPanelAnimation.animation.SCALE: + reverseAnimationOptions = { + transitionInClass: '_md-panel-animate-scale-out _md-panel-animate-leave' + }; + + var closeScale = animator.calculateZoomToOrigin( + panelEl, this._closeTo) || ''; + closeTo = animator.toTransformCss(closeScale + ' ' + panelTransform); + break; + + case MdPanelAnimation.animation.FADE: + reverseAnimationOptions = { + transitionInClass: '_md-panel-animate-fade-out _md-panel-animate-leave' + }; + break; + + default: + if (angular.isString(this._animationClass)) { + reverseAnimationOptions = { + transitionOutClass: this._animationClass + }; + } else { + reverseAnimationOptions = { + transitionInClass: this._animationClass['close'], + transitionOutClass: this._animationClass['open'] + }; + } + } + + return animator + .translate3d(panelEl, closeFrom, closeTo, reverseAnimationOptions); +}; + + +/** + * Set the height and width to match the panel if not provided. + * @param {!angular.JQLite} panelEl + * @private + */ +MdPanelAnimation.prototype._fixBounds = function(panelEl) { + var panelWidth = panelEl[0].offsetWidth; + var panelHeight = panelEl[0].offsetHeight; + + if (this._openFrom && this._openFrom.bounds.height == null) { + this._openFrom.bounds.height = panelHeight; + } + if (this._openFrom && this._openFrom.bounds.width == null) { + this._openFrom.bounds.width = panelWidth; + } + if (this._closeTo && this._closeTo.bounds.height == null) { + this._closeTo.bounds.height = panelHeight; + } + if (this._closeTo && this._closeTo.bounds.width == null) { + this._closeTo.bounds.width = panelWidth; + } +}; + + +/** + * Identify the bounding RECT for the target element. + * @param {!angular.JQLite} element + * @returns {{element: !angular.JQLite|undefined, bounds: !DOMRect}} + * @private + */ +MdPanelAnimation.prototype._getBoundingClientRect = function(element) { + if (element instanceof angular.element) { + return { + element: element, + bounds: element[0].getBoundingClientRect() + }; + } +}; + + +/***************************************************************************** + * Util Methods * + *****************************************************************************/ + +/** + * Returns the angular element associated with a css selector or element. + * @param el {string|!angular.JQLite|!Element} + * @returns {!angular.JQLite} + */ +function getElement(el) { + var queryResult = angular.isString(el) ? + document.querySelector(el) : el; + return angular.element(queryResult); +} + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.components.progressCircular + * @description Module for a circular progressbar + */ + +angular.module('material.components.progressCircular', ['material.core']); + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.components.progressLinear + * @description Linear Progress module! + */ +MdProgressLinearDirective.$inject = ["$mdTheming", "$mdUtil", "$log"]; +angular.module('material.components.progressLinear', [ + 'material.core' +]) + .directive('mdProgressLinear', MdProgressLinearDirective); + +/** + * @ngdoc directive + * @name mdProgressLinear + * @module material.components.progressLinear + * @restrict E + * + * @description + * The linear progress directive is used to make loading content + * in your app as delightful and painless as possible by minimizing + * the amount of visual change a user sees before they can view + * and interact with content. + * + * Each operation should only be represented by one activity indicator + * For example: one refresh operation should not display both a + * refresh bar and an activity circle. + * + * For operations where the percentage of the operation completed + * can be determined, use a determinate indicator. They give users + * a quick sense of how long an operation will take. + * + * For operations where the user is asked to wait a moment while + * something finishes up, and it’s not necessary to expose what's + * happening behind the scenes and how long it will take, use an + * indeterminate indicator. + * + * @param {string} md-mode Select from one of four modes: determinate, indeterminate, buffer or query. + * + * Note: if the `md-mode` value is set as undefined or specified as 1 of the four (4) valid modes, then `indeterminate` + * will be auto-applied as the mode. + * + * Note: if not configured, the `md-mode="indeterminate"` will be auto injected as an attribute. If `value=""` is also specified, however, + * then `md-mode="determinate"` would be auto-injected instead. + * @param {number=} value In determinate and buffer modes, this number represents the percentage of the primary progress bar. Default: 0 + * @param {number=} md-buffer-value In the buffer mode, this number represents the percentage of the secondary progress bar. Default: 0 + * @param {boolean=} ng-disabled Determines whether to disable the progress element. + * + * @usage + * + * + * + * + * + * + * + * + * + * + * + */ +function MdProgressLinearDirective($mdTheming, $mdUtil, $log) { + var MODE_DETERMINATE = "determinate"; + var MODE_INDETERMINATE = "indeterminate"; + var MODE_BUFFER = "buffer"; + var MODE_QUERY = "query"; + var DISABLED_CLASS = "_md-progress-linear-disabled"; + + return { + restrict: 'E', + template: '
      ' + + '
      ' + + '
      ' + + '
      ' + + '
      ', + compile: compile + }; + + function compile(tElement, tAttrs, transclude) { + tElement.attr('aria-valuemin', 0); + tElement.attr('aria-valuemax', 100); + tElement.attr('role', 'progressbar'); + + return postLink; + } + function postLink(scope, element, attr) { + $mdTheming(element); + + var lastMode; + var isDisabled = attr.hasOwnProperty('disabled'); + var toVendorCSS = $mdUtil.dom.animator.toCss; + var bar1 = angular.element(element[0].querySelector('.md-bar1')); + var bar2 = angular.element(element[0].querySelector('.md-bar2')); + var container = angular.element(element[0].querySelector('.md-container')); + + element + .attr('md-mode', mode()) + .toggleClass(DISABLED_CLASS, isDisabled); + + validateMode(); + watchAttributes(); + + /** + * Watch the value, md-buffer-value, and md-mode attributes + */ + function watchAttributes() { + attr.$observe('value', function(value) { + var percentValue = clamp(value); + element.attr('aria-valuenow', percentValue); + + if (mode() != MODE_QUERY) animateIndicator(bar2, percentValue); + }); + + attr.$observe('mdBufferValue', function(value) { + animateIndicator(bar1, clamp(value)); + }); + + attr.$observe('disabled', function(value) { + if (value === true || value === false) { + isDisabled = !!value; + } else { + isDisabled = angular.isDefined(value); + } + + element.toggleClass(DISABLED_CLASS, isDisabled); + container.toggleClass(lastMode, !isDisabled); + }); + + attr.$observe('mdMode', function(mode) { + if (lastMode) container.removeClass( lastMode ); + + switch( mode ) { + case MODE_QUERY: + case MODE_BUFFER: + case MODE_DETERMINATE: + case MODE_INDETERMINATE: + container.addClass( lastMode = "md-mode-" + mode ); + break; + default: + container.addClass( lastMode = "md-mode-" + MODE_INDETERMINATE ); + break; + } + }); + } + + /** + * Auto-defaults the mode to either `determinate` or `indeterminate` mode; if not specified + */ + function validateMode() { + if ( angular.isUndefined(attr.mdMode) ) { + var hasValue = angular.isDefined(attr.value); + var mode = hasValue ? MODE_DETERMINATE : MODE_INDETERMINATE; + var info = "Auto-adding the missing md-mode='{0}' to the ProgressLinear element"; + + //$log.debug( $mdUtil.supplant(info, [mode]) ); + + element.attr("md-mode", mode); + attr.mdMode = mode; + } + } + + /** + * Is the md-mode a valid option? + */ + function mode() { + var value = (attr.mdMode || "").trim(); + if ( value ) { + switch(value) { + case MODE_DETERMINATE: + case MODE_INDETERMINATE: + case MODE_BUFFER: + case MODE_QUERY: + break; + default: + value = MODE_INDETERMINATE; + break; + } + } + return value; + } + + /** + * Manually set CSS to animate the Determinate indicator based on the specified + * percentage value (0-100). + */ + function animateIndicator(target, value) { + if ( isDisabled || !mode() ) return; + + var to = $mdUtil.supplant("translateX({0}%) scale({1},1)", [ (value-100)/2, value/100 ]); + var styles = toVendorCSS({ transform : to }); + angular.element(target).css( styles ); + } + } + + /** + * Clamps the value to be between 0 and 100. + * @param {number} value The value to clamp. + * @returns {number} + */ + function clamp(value) { + return Math.max(0, Math.min(value || 0, 100)); + } +} + + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.components.radioButton + * @description radioButton module! + */ +mdRadioGroupDirective.$inject = ["$mdUtil", "$mdConstant", "$mdTheming", "$timeout"]; +mdRadioButtonDirective.$inject = ["$mdAria", "$mdUtil", "$mdTheming"]; +angular.module('material.components.radioButton', [ + 'material.core' +]) + .directive('mdRadioGroup', mdRadioGroupDirective) + .directive('mdRadioButton', mdRadioButtonDirective); + +/** + * @ngdoc directive + * @module material.components.radioButton + * @name mdRadioGroup + * + * @restrict E + * + * @description + * The `` directive identifies a grouping + * container for the 1..n grouped radio buttons; specified using nested + * `` tags. + * + * As per the [material design spec](http://www.google.com/design/spec/style/color.html#color-ui-color-application) + * the radio button is in the accent color by default. The primary color palette may be used with + * the `md-primary` class. + * + * Note: `` and `` handle tabindex differently + * than the native `` controls. Whereas the native controls + * force the user to tab through all the radio buttons, `` + * is focusable, and by default the ``s are not. + * + * @param {string} ng-model Assignable angular expression to data-bind to. + * @param {boolean=} md-no-ink Use of attribute indicates flag to disable ink ripple effects. + * + * @usage + * + * + * + * + * + * {{ d.label }} + * + * + * + * + * + * + */ +function mdRadioGroupDirective($mdUtil, $mdConstant, $mdTheming, $timeout) { + RadioGroupController.prototype = createRadioGroupControllerProto(); + + return { + restrict: 'E', + controller: ['$element', RadioGroupController], + require: ['mdRadioGroup', '?ngModel'], + link: { pre: linkRadioGroup } + }; + + function linkRadioGroup(scope, element, attr, ctrls) { + element.addClass('_md'); // private md component indicator for styling + $mdTheming(element); + + var rgCtrl = ctrls[0]; + var ngModelCtrl = ctrls[1] || $mdUtil.fakeNgModel(); + + rgCtrl.init(ngModelCtrl); + + scope.mouseActive = false; + + element + .attr({ + 'role': 'radiogroup', + 'tabIndex': element.attr('tabindex') || '0' + }) + .on('keydown', keydownListener) + .on('mousedown', function(event) { + scope.mouseActive = true; + $timeout(function() { + scope.mouseActive = false; + }, 100); + }) + .on('focus', function() { + if(scope.mouseActive === false) { + rgCtrl.$element.addClass('md-focused'); + } + }) + .on('blur', function() { + rgCtrl.$element.removeClass('md-focused'); + }); + + /** + * + */ + function setFocus() { + if (!element.hasClass('md-focused')) { element.addClass('md-focused'); } + } + + /** + * + */ + function keydownListener(ev) { + var keyCode = ev.which || ev.keyCode; + + // Only listen to events that we originated ourselves + // so that we don't trigger on things like arrow keys in + // inputs. + + if (keyCode != $mdConstant.KEY_CODE.ENTER && + ev.currentTarget != ev.target) { + return; + } + + switch (keyCode) { + case $mdConstant.KEY_CODE.LEFT_ARROW: + case $mdConstant.KEY_CODE.UP_ARROW: + ev.preventDefault(); + rgCtrl.selectPrevious(); + setFocus(); + break; + + case $mdConstant.KEY_CODE.RIGHT_ARROW: + case $mdConstant.KEY_CODE.DOWN_ARROW: + ev.preventDefault(); + rgCtrl.selectNext(); + setFocus(); + break; + + case $mdConstant.KEY_CODE.ENTER: + var form = angular.element($mdUtil.getClosest(element[0], 'form')); + if (form.length > 0) { + form.triggerHandler('submit'); + } + break; + } + + } + } + + function RadioGroupController($element) { + this._radioButtonRenderFns = []; + this.$element = $element; + } + + function createRadioGroupControllerProto() { + return { + init: function(ngModelCtrl) { + this._ngModelCtrl = ngModelCtrl; + this._ngModelCtrl.$render = angular.bind(this, this.render); + }, + add: function(rbRender) { + this._radioButtonRenderFns.push(rbRender); + }, + remove: function(rbRender) { + var index = this._radioButtonRenderFns.indexOf(rbRender); + if (index !== -1) { + this._radioButtonRenderFns.splice(index, 1); + } + }, + render: function() { + this._radioButtonRenderFns.forEach(function(rbRender) { + rbRender(); + }); + }, + setViewValue: function(value, eventType) { + this._ngModelCtrl.$setViewValue(value, eventType); + // update the other radio buttons as well + this.render(); + }, + getViewValue: function() { + return this._ngModelCtrl.$viewValue; + }, + selectNext: function() { + return changeSelectedButton(this.$element, 1); + }, + selectPrevious: function() { + return changeSelectedButton(this.$element, -1); + }, + setActiveDescendant: function (radioId) { + this.$element.attr('aria-activedescendant', radioId); + }, + isDisabled: function() { + return this.$element[0].hasAttribute('disabled'); + } + }; + } + /** + * Change the radio group's selected button by a given increment. + * If no button is selected, select the first button. + */ + function changeSelectedButton(parent, increment) { + // Coerce all child radio buttons into an array, then wrap then in an iterator + var buttons = $mdUtil.iterator(parent[0].querySelectorAll('md-radio-button'), true); + + if (buttons.count()) { + var validate = function (button) { + // If disabled, then NOT valid + return !angular.element(button).attr("disabled"); + }; + + var selected = parent[0].querySelector('md-radio-button.md-checked'); + var target = buttons[increment < 0 ? 'previous' : 'next'](selected, validate) || buttons.first(); + + // Activate radioButton's click listener (triggerHandler won't create a real click event) + angular.element(target).triggerHandler('click'); + + + } + } + +} + +/** + * @ngdoc directive + * @module material.components.radioButton + * @name mdRadioButton + * + * @restrict E + * + * @description + * The ``directive is the child directive required to be used within `` elements. + * + * While similar to the `` directive, + * the `` directive provides ink effects, ARIA support, and + * supports use within named radio groups. + * + * @param {string} ngModel Assignable angular expression to data-bind to. + * @param {string=} ngChange Angular expression to be executed when input changes due to user + * interaction with the input element. + * @param {string} ngValue Angular expression which sets the value to which the expression should + * be set when selected. + * @param {string} value The value to which the expression should be set when selected. + * @param {string=} name Property name of the form under which the control is published. + * @param {string=} aria-label Adds label to radio button for accessibility. + * Defaults to radio button's text. If no text content is available, a warning will be logged. + * + * @usage + * + * + * + * Label 1 + * + * + * + * Green + * + * + * + * + */ +function mdRadioButtonDirective($mdAria, $mdUtil, $mdTheming) { + + var CHECKED_CSS = 'md-checked'; + + return { + restrict: 'E', + require: '^mdRadioGroup', + transclude: true, + template: '
      ' + + '
      ' + + '
      ' + + '
      ' + + '
      ', + link: link + }; + + function link(scope, element, attr, rgCtrl) { + var lastChecked; + + $mdTheming(element); + configureAria(element, scope); + + initialize(); + + /** + * + */ + function initialize() { + if (!rgCtrl) { + throw 'RadioButton: No RadioGroupController could be found.'; + } + + rgCtrl.add(render); + attr.$observe('value', render); + + element + .on('click', listener) + .on('$destroy', function() { + rgCtrl.remove(render); + }); + } + + /** + * + */ + function listener(ev) { + if (element[0].hasAttribute('disabled') || rgCtrl.isDisabled()) return; + + scope.$apply(function() { + rgCtrl.setViewValue(attr.value, ev && ev.type); + }); + } + + /** + * Add or remove the `.md-checked` class from the RadioButton (and conditionally its parent). + * Update the `aria-activedescendant` attribute. + */ + function render() { + var checked = (rgCtrl.getViewValue() == attr.value); + if (checked === lastChecked) { + return; + } + + lastChecked = checked; + element.attr('aria-checked', checked); + + if (checked) { + markParentAsChecked(true); + element.addClass(CHECKED_CSS); + + rgCtrl.setActiveDescendant(element.attr('id')); + + } else { + markParentAsChecked(false); + element.removeClass(CHECKED_CSS); + } + + /** + * If the radioButton is inside a div, then add class so highlighting will work... + */ + function markParentAsChecked(addClass ) { + if ( element.parent()[0].nodeName != "MD-RADIO-GROUP") { + element.parent()[ !!addClass ? 'addClass' : 'removeClass'](CHECKED_CSS); + } + + } + } + + /** + * Inject ARIA-specific attributes appropriate for each radio button + */ + function configureAria( element, scope ){ + scope.ariaId = buildAriaID(); + + element.attr({ + 'id' : scope.ariaId, + 'role' : 'radio', + 'aria-checked' : 'false' + }); + + $mdAria.expectWithText(element, 'aria-label'); + + /** + * Build a unique ID for each radio button that will be used with aria-activedescendant. + * Preserve existing ID if already specified. + * @returns {*|string} + */ + function buildAriaID() { + return attr.id || ( 'radio' + "_" + $mdUtil.nextUid() ); + } + } + } +} + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.components.select + */ + +/*************************************************** + + ### TODO - POST RC1 ### + - [ ] Abstract placement logic in $mdSelect service to $mdMenu service + + ***************************************************/ + +SelectDirective.$inject = ["$mdSelect", "$mdUtil", "$mdConstant", "$mdTheming", "$mdAria", "$compile", "$parse"]; +SelectMenuDirective.$inject = ["$parse", "$mdUtil", "$mdConstant", "$mdTheming"]; +OptionDirective.$inject = ["$mdButtonInkRipple", "$mdUtil"]; +SelectProvider.$inject = ["$$interimElementProvider"]; +var SELECT_EDGE_MARGIN = 8; +var selectNextId = 0; +var CHECKBOX_SELECTION_INDICATOR = + angular.element('
      '); + +angular.module('material.components.select', [ + 'material.core', + 'material.components.backdrop' + ]) + .directive('mdSelect', SelectDirective) + .directive('mdSelectMenu', SelectMenuDirective) + .directive('mdOption', OptionDirective) + .directive('mdOptgroup', OptgroupDirective) + .directive('mdSelectHeader', SelectHeaderDirective) + .provider('$mdSelect', SelectProvider); + +/** + * @ngdoc directive + * @name mdSelect + * @restrict E + * @module material.components.select + * + * @description Displays a select box, bound to an ng-model. + * + * When the select is required and uses a floating label, then the label will automatically contain + * an asterisk (`*`). This behavior can be disabled by using the `md-no-asterisk` attribute. + * + * By default, the select will display with an underline to match other form elements. This can be + * disabled by applying the `md-no-underline` CSS class. + * + * ### Option Params + * + * When applied, `md-option-empty` will mark the option as "empty" allowing the option to clear the + * select and put it back in it's default state. You may supply this attribute on any option you + * wish, however, it is automatically applied to an option whose `value` or `ng-value` are not + * defined. + * + * **Automatically Applied** + * + * - `` + * - `` + * - `` + * - `` + * - `` + * + * **NOT Automatically Applied** + * + * - `` + * - `` + * - `` + * - `` (this evaluates to the string `"undefined"`) + * - <md-option ng-value="{{someValueThatMightBeUndefined}}"> + * + * **Note:** A value of `undefined` ***is considered a valid value*** (and does not auto-apply this + * attribute) since you may wish this to be your "Not Available" or "None" option. + * + * **Note:** Using the `value` attribute (as opposed to `ng-value`) always evaluates to a string, so + * `value="null"` will require the test `ng-if="myValue != 'null'"` rather than `ng-if="!myValue"`. + * + * @param {expression} ng-model The model! + * @param {boolean=} multiple Whether it's multiple. + * @param {expression=} md-on-close Expression to be evaluated when the select is closed. + * @param {expression=} md-on-open Expression to be evaluated when opening the select. + * Will hide the select options and show a spinner until the evaluated promise resolves. + * @param {expression=} md-selected-text Expression to be evaluated that will return a string + * to be displayed as a placeholder in the select input box when it is closed. + * @param {string=} placeholder Placeholder hint text. + * @param md-no-asterisk {boolean=} When set to true, an asterisk will not be appended to the + * floating label. **Note:** This attribute is only evaluated once; it is not watched. + * @param {string=} aria-label Optional label for accessibility. Only necessary if no placeholder or + * explicit label is present. + * @param {string=} md-container-class Class list to get applied to the `.md-select-menu-container` + * element (for custom styling). + * + * @usage + * With a placeholder (label and aria-label are added dynamically) + * + * + * + * {{ opt }} + * + * + * + * + * With an explicit label + * + * + * + * + * {{ opt }} + * + * + * + * + * With a select-header + * + * When a developer needs to put more than just a text label in the + * md-select-menu, they should use the md-select-header. + * The user can put custom HTML inside of the header and style it to their liking. + * One common use case of this would be a sticky search bar. + * + * When using the md-select-header the labels that would previously be added to the + * OptGroupDirective are ignored. + * + * + * + * + * + * Neighborhoods - + * + * {{ opt }} + * + * + * + * + * ## Selects and object equality + * When using a `md-select` to pick from a list of objects, it is important to realize how javascript handles + * equality. Consider the following example: + * + * angular.controller('MyCtrl', function($scope) { + * $scope.users = [ + * { id: 1, name: 'Bob' }, + * { id: 2, name: 'Alice' }, + * { id: 3, name: 'Steve' } + * ]; + * $scope.selectedUser = { id: 1, name: 'Bob' }; + * }); + * + * + *
      + * + * {{ user.name }} + * + *
      + *
      + * + * At first one might expect that the select should be populated with "Bob" as the selected user. However, + * this is not true. To determine whether something is selected, + * `ngModelController` is looking at whether `$scope.selectedUser == (any user in $scope.users);`; + * + * Javascript's `==` operator does not check for deep equality (ie. that all properties + * on the object are the same), but instead whether the objects are *the same object in memory*. + * In this case, we have two instances of identical objects, but they exist in memory as unique + * entities. Because of this, the select will have no value populated for a selected user. + * + * To get around this, `ngModelController` provides a `track by` option that allows us to specify a different + * expression which will be used for the equality operator. As such, we can update our `html` to + * make use of this by specifying the `ng-model-options="{trackBy: '$value.id'}"` on the `md-select` + * element. This converts our equality expression to be + * `$scope.selectedUser.id == (any id in $scope.users.map(function(u) { return u.id; }));` + * which results in Bob being selected as desired. + * + * Working HTML: + * + *
      + * + * {{ user.name }} + * + *
      + *
      + */ +function SelectDirective($mdSelect, $mdUtil, $mdConstant, $mdTheming, $mdAria, $compile, $parse) { + var keyCodes = $mdConstant.KEY_CODE; + var NAVIGATION_KEYS = [keyCodes.SPACE, keyCodes.ENTER, keyCodes.UP_ARROW, keyCodes.DOWN_ARROW]; + + return { + restrict: 'E', + require: ['^?mdInputContainer', 'mdSelect', 'ngModel', '?^form'], + compile: compile, + controller: function() { + } // empty placeholder controller to be initialized in link + }; + + function compile(element, attr) { + // add the select value that will hold our placeholder or selected option value + var valueEl = angular.element(''); + valueEl.append(''); + valueEl.addClass('md-select-value'); + if (!valueEl[0].hasAttribute('id')) { + valueEl.attr('id', 'select_value_label_' + $mdUtil.nextUid()); + } + + // There's got to be an md-content inside. If there's not one, let's add it. + if (!element.find('md-content').length) { + element.append(angular.element('').append(element.contents())); + } + + + // Add progress spinner for md-options-loading + if (attr.mdOnOpen) { + + // Show progress indicator while loading async + // Use ng-hide for `display:none` so the indicator does not interfere with the options list + element + .find('md-content') + .prepend(angular.element( + '
      ' + + ' ' + + '
      ' + )); + + // Hide list [of item options] while loading async + element + .find('md-option') + .attr('ng-show', '$$loadingAsyncDone'); + } + + if (attr.name) { + var autofillClone = angular.element(', + + + + + + + + + + + + + + + + + + + + + + + + + +
      DirectiveHowSourceRendered
      ng-bind-htmlAutomatically uses $sanitize
      <div ng-bind-html="snippet">
      </div>
      ng-bind-htmlBypass $sanitize by explicitly trusting the dangerous value +
      <div ng-bind-html="deliberatelyTrustDangerousSnippet()">
      +</div>
      +
      ng-bindAutomatically escapes
      <div ng-bind="snippet">
      </div>
      +
      + + + it('should sanitize the html snippet by default', function() { + expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()). + toBe('

      an html\nclick here\nsnippet

      '); + }); + + it('should inline raw snippet if bound to a trusted value', function() { + expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()). + toBe("

      an html\n" + + "click here\n" + + "snippet

      "); + }); + + it('should escape snippet without any filter', function() { + expect(element(by.css('#bind-default div')).getInnerHtml()). + toBe("<p style=\"color:blue\">an html\n" + + "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" + + "snippet</p>"); + }); + + it('should update', function() { + element(by.model('snippet')).clear(); + element(by.model('snippet')).sendKeys('new text'); + expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()). + toBe('new text'); + expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()).toBe( + 'new text'); + expect(element(by.css('#bind-default div')).getInnerHtml()).toBe( + "new <b onclick=\"alert(1)\">text</b>"); + }); +
      + + */ + + +/** + * @ngdoc provider + * @name $sanitizeProvider + * + * @description + * Creates and configures {@link $sanitize} instance. + */ +function $SanitizeProvider() { + var svgEnabled = false; + + this.$get = ['$$sanitizeUri', function($$sanitizeUri) { + if (svgEnabled) { + extend(validElements, svgElements); + } + return function(html) { + var buf = []; + htmlParser(html, htmlSanitizeWriter(buf, function(uri, isImage) { + return !/^unsafe:/.test($$sanitizeUri(uri, isImage)); + })); + return buf.join(''); + }; + }]; + + + /** + * @ngdoc method + * @name $sanitizeProvider#enableSvg + * @kind function + * + * @description + * Enables a subset of svg to be supported by the sanitizer. + * + *
      + *

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

      + * + *

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

      + * + *
      + * + *
      
      +   *   .rootOfTheIncludedContent svg {
      +   *     overflow: hidden !important;
      +   *   }
      +   *   
      + *
      + * + * @param {boolean=} flag Enable or disable SVG support in the sanitizer. + * @returns {boolean|ng.$sanitizeProvider} Returns the currently configured value if called + * without an argument or self for chaining otherwise. + */ + this.enableSvg = function(enableSvg) { + if (isDefined(enableSvg)) { + svgEnabled = enableSvg; + return this; + } else { + return svgEnabled; + } + }; + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Private stuff + ////////////////////////////////////////////////////////////////////////////////////////////////// + + bind = angular.bind; + extend = angular.extend; + forEach = angular.forEach; + isDefined = angular.isDefined; + lowercase = angular.lowercase; + noop = angular.noop; + + htmlParser = htmlParserImpl; + htmlSanitizeWriter = htmlSanitizeWriterImpl; + + // Regular Expressions for parsing tags and attributes + var SURROGATE_PAIR_REGEXP = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g, + // Match everything outside of normal chars and " (quote character) + NON_ALPHANUMERIC_REGEXP = /([^\#-~ |!])/g; + + + // Good source of info about elements and attributes + // http://dev.w3.org/html5/spec/Overview.html#semantics + // http://simon.html5.org/html-elements + + // Safe Void Elements - HTML5 + // http://dev.w3.org/html5/spec/Overview.html#void-elements + var voidElements = toMap("area,br,col,hr,img,wbr"); + + // Elements that you can, intentionally, leave open (and which close themselves) + // http://dev.w3.org/html5/spec/Overview.html#optional-tags + var optionalEndTagBlockElements = toMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"), + optionalEndTagInlineElements = toMap("rp,rt"), + optionalEndTagElements = extend({}, + optionalEndTagInlineElements, + optionalEndTagBlockElements); + + // Safe Block Elements - HTML5 + var blockElements = extend({}, optionalEndTagBlockElements, toMap("address,article," + + "aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5," + + "h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,section,table,ul")); + + // Inline Elements - HTML5 + var inlineElements = extend({}, optionalEndTagInlineElements, toMap("a,abbr,acronym,b," + + "bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s," + + "samp,small,span,strike,strong,sub,sup,time,tt,u,var")); + + // SVG Elements + // https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Elements + // Note: the elements animate,animateColor,animateMotion,animateTransform,set are intentionally omitted. + // They can potentially allow for arbitrary javascript to be executed. See #11290 + var svgElements = toMap("circle,defs,desc,ellipse,font-face,font-face-name,font-face-src,g,glyph," + + "hkern,image,linearGradient,line,marker,metadata,missing-glyph,mpath,path,polygon,polyline," + + "radialGradient,rect,stop,svg,switch,text,title,tspan"); + + // Blocked Elements (will be stripped) + var blockedElements = toMap("script,style"); + + var validElements = extend({}, + voidElements, + blockElements, + inlineElements, + optionalEndTagElements); + + //Attributes that have href and hence need to be sanitized + var uriAttrs = toMap("background,cite,href,longdesc,src,xlink:href"); + + var htmlAttrs = toMap('abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,' + + 'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,' + + 'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,' + + 'scope,scrolling,shape,size,span,start,summary,tabindex,target,title,type,' + + 'valign,value,vspace,width'); + + // SVG attributes (without "id" and "name" attributes) + // https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Attributes + var svgAttrs = toMap('accent-height,accumulate,additive,alphabetic,arabic-form,ascent,' + + 'baseProfile,bbox,begin,by,calcMode,cap-height,class,color,color-rendering,content,' + + 'cx,cy,d,dx,dy,descent,display,dur,end,fill,fill-rule,font-family,font-size,font-stretch,' + + 'font-style,font-variant,font-weight,from,fx,fy,g1,g2,glyph-name,gradientUnits,hanging,' + + 'height,horiz-adv-x,horiz-origin-x,ideographic,k,keyPoints,keySplines,keyTimes,lang,' + + 'marker-end,marker-mid,marker-start,markerHeight,markerUnits,markerWidth,mathematical,' + + 'max,min,offset,opacity,orient,origin,overline-position,overline-thickness,panose-1,' + + 'path,pathLength,points,preserveAspectRatio,r,refX,refY,repeatCount,repeatDur,' + + 'requiredExtensions,requiredFeatures,restart,rotate,rx,ry,slope,stemh,stemv,stop-color,' + + 'stop-opacity,strikethrough-position,strikethrough-thickness,stroke,stroke-dasharray,' + + 'stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,stroke-opacity,' + + 'stroke-width,systemLanguage,target,text-anchor,to,transform,type,u1,u2,underline-position,' + + 'underline-thickness,unicode,unicode-range,units-per-em,values,version,viewBox,visibility,' + + 'width,widths,x,x-height,x1,x2,xlink:actuate,xlink:arcrole,xlink:role,xlink:show,xlink:title,' + + 'xlink:type,xml:base,xml:lang,xml:space,xmlns,xmlns:xlink,y,y1,y2,zoomAndPan', true); + + var validAttrs = extend({}, + uriAttrs, + svgAttrs, + htmlAttrs); + + function toMap(str, lowercaseKeys) { + var obj = {}, items = str.split(','), i; + for (i = 0; i < items.length; i++) { + obj[lowercaseKeys ? lowercase(items[i]) : items[i]] = true; + } + return obj; + } + + var inertBodyElement; + (function(window) { + var doc; + if (window.document && window.document.implementation) { + doc = window.document.implementation.createHTMLDocument("inert"); + } else { + throw $sanitizeMinErr('noinert', "Can't create an inert html document"); + } + var docElement = doc.documentElement || doc.getDocumentElement(); + var bodyElements = docElement.getElementsByTagName('body'); + + // usually there should be only one body element in the document, but IE doesn't have any, so we need to create one + if (bodyElements.length === 1) { + inertBodyElement = bodyElements[0]; + } else { + var html = doc.createElement('html'); + inertBodyElement = doc.createElement('body'); + html.appendChild(inertBodyElement); + doc.appendChild(html); + } + })(window); + + /** + * @example + * htmlParser(htmlString, { + * start: function(tag, attrs) {}, + * end: function(tag) {}, + * chars: function(text) {}, + * comment: function(text) {} + * }); + * + * @param {string} html string + * @param {object} handler + */ + function htmlParserImpl(html, handler) { + if (html === null || html === undefined) { + html = ''; + } else if (typeof html !== 'string') { + html = '' + html; + } + inertBodyElement.innerHTML = html; + + //mXSS protection + var mXSSAttempts = 5; + do { + if (mXSSAttempts === 0) { + throw $sanitizeMinErr('uinput', "Failed to sanitize html because the input is unstable"); + } + mXSSAttempts--; + + // strip custom-namespaced attributes on IE<=11 + if (window.document.documentMode) { + stripCustomNsAttrs(inertBodyElement); + } + html = inertBodyElement.innerHTML; //trigger mXSS + inertBodyElement.innerHTML = html; + } while (html !== inertBodyElement.innerHTML); + + var node = inertBodyElement.firstChild; + while (node) { + switch (node.nodeType) { + case 1: // ELEMENT_NODE + handler.start(node.nodeName.toLowerCase(), attrToMap(node.attributes)); + break; + case 3: // TEXT NODE + handler.chars(node.textContent); + break; + } + + var nextNode; + if (!(nextNode = node.firstChild)) { + if (node.nodeType == 1) { + handler.end(node.nodeName.toLowerCase()); + } + nextNode = node.nextSibling; + if (!nextNode) { + while (nextNode == null) { + node = node.parentNode; + if (node === inertBodyElement) break; + nextNode = node.nextSibling; + if (node.nodeType == 1) { + handler.end(node.nodeName.toLowerCase()); + } + } + } + } + node = nextNode; + } + + while (node = inertBodyElement.firstChild) { + inertBodyElement.removeChild(node); + } + } + + function attrToMap(attrs) { + var map = {}; + for (var i = 0, ii = attrs.length; i < ii; i++) { + var attr = attrs[i]; + map[attr.name] = attr.value; + } + return map; + } + + + /** + * Escapes all potentially dangerous characters, so that the + * resulting string can be safely inserted into attribute or + * element text. + * @param value + * @returns {string} escaped text + */ + function encodeEntities(value) { + return value. + replace(/&/g, '&'). + replace(SURROGATE_PAIR_REGEXP, function(value) { + var hi = value.charCodeAt(0); + var low = value.charCodeAt(1); + return '&#' + (((hi - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000) + ';'; + }). + replace(NON_ALPHANUMERIC_REGEXP, function(value) { + return '&#' + value.charCodeAt(0) + ';'; + }). + replace(//g, '>'); + } + + /** + * create an HTML/XML writer which writes to buffer + * @param {Array} buf use buf.join('') to get out sanitized html string + * @returns {object} in the form of { + * start: function(tag, attrs) {}, + * end: function(tag) {}, + * chars: function(text) {}, + * comment: function(text) {} + * } + */ + function htmlSanitizeWriterImpl(buf, uriValidator) { + var ignoreCurrentElement = false; + var out = bind(buf, buf.push); + return { + start: function(tag, attrs) { + tag = lowercase(tag); + if (!ignoreCurrentElement && blockedElements[tag]) { + ignoreCurrentElement = tag; + } + if (!ignoreCurrentElement && validElements[tag] === true) { + out('<'); + out(tag); + forEach(attrs, function(value, key) { + var lkey = lowercase(key); + var isImage = (tag === 'img' && lkey === 'src') || (lkey === 'background'); + if (validAttrs[lkey] === true && + (uriAttrs[lkey] !== true || uriValidator(value, isImage))) { + out(' '); + out(key); + out('="'); + out(encodeEntities(value)); + out('"'); + } + }); + out('>'); + } + }, + end: function(tag) { + tag = lowercase(tag); + if (!ignoreCurrentElement && validElements[tag] === true && voidElements[tag] !== true) { + out(''); + } + if (tag == ignoreCurrentElement) { + ignoreCurrentElement = false; + } + }, + chars: function(chars) { + if (!ignoreCurrentElement) { + out(encodeEntities(chars)); + } + } + }; + } + + + /** + * When IE9-11 comes across an unknown namespaced attribute e.g. 'xlink:foo' it adds 'xmlns:ns1' attribute to declare + * ns1 namespace and prefixes the attribute with 'ns1' (e.g. 'ns1:xlink:foo'). This is undesirable since we don't want + * to allow any of these custom attributes. This method strips them all. + * + * @param node Root element to process + */ + function stripCustomNsAttrs(node) { + if (node.nodeType === window.Node.ELEMENT_NODE) { + var attrs = node.attributes; + for (var i = 0, l = attrs.length; i < l; i++) { + var attrNode = attrs[i]; + var attrName = attrNode.name.toLowerCase(); + if (attrName === 'xmlns:ns1' || attrName.lastIndexOf('ns1:', 0) === 0) { + node.removeAttributeNode(attrNode); + i--; + l--; + } + } + } + + var nextNode = node.firstChild; + if (nextNode) { + stripCustomNsAttrs(nextNode); + } + + nextNode = node.nextSibling; + if (nextNode) { + stripCustomNsAttrs(nextNode); + } + } +} + +function sanitizeText(chars) { + var buf = []; + var writer = htmlSanitizeWriter(buf, noop); + writer.chars(chars); + return buf.join(''); +} + + +// define ngSanitize module and register $sanitize service +angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider); + +/** + * @ngdoc filter + * @name linky + * @kind function + * + * @description + * Finds links in text input and turns them into html links. Supports `http/https/ftp/mailto` and + * plain email address links. + * + * Requires the {@link ngSanitize `ngSanitize`} module to be installed. + * + * @param {string} text Input text. + * @param {string} target Window (`_blank|_self|_parent|_top`) or named frame to open links in. + * @param {object|function(url)} [attributes] Add custom attributes to the link element. + * + * Can be one of: + * + * - `object`: A map of attributes + * - `function`: Takes the url as a parameter and returns a map of attributes + * + * If the map of attributes contains a value for `target`, it overrides the value of + * the target parameter. + * + * + * @returns {string} Html-linkified and {@link $sanitize sanitized} text. + * + * @usage + + * + * @example + + +
      + Snippet: + + + + + + + + + + + + + + + + + + + + + + + + + + +
      FilterSourceRendered
      linky filter +
      <div ng-bind-html="snippet | linky">
      </div>
      +
      +
      +
      linky target +
      <div ng-bind-html="snippetWithSingleURL | linky:'_blank'">
      </div>
      +
      +
      +
      linky custom attributes +
      <div ng-bind-html="snippetWithSingleURL | linky:'_self':{rel: 'nofollow'}">
      </div>
      +
      +
      +
      no filter
      <div ng-bind="snippet">
      </div>
      + + + angular.module('linkyExample', ['ngSanitize']) + .controller('ExampleController', ['$scope', function($scope) { + $scope.snippet = + 'Pretty text with some links:\n'+ + 'http://angularjs.org/,\n'+ + 'mailto:us@somewhere.org,\n'+ + 'another@somewhere.org,\n'+ + 'and one more: ftp://127.0.0.1/.'; + $scope.snippetWithSingleURL = 'http://angularjs.org/'; + }]); + + + it('should linkify the snippet with urls', function() { + expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()). + toBe('Pretty text with some links: http://angularjs.org/, us@somewhere.org, ' + + 'another@somewhere.org, and one more: ftp://127.0.0.1/.'); + expect(element.all(by.css('#linky-filter a')).count()).toEqual(4); + }); + + it('should not linkify snippet without the linky filter', function() { + expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()). + toBe('Pretty text with some links: http://angularjs.org/, mailto:us@somewhere.org, ' + + 'another@somewhere.org, and one more: ftp://127.0.0.1/.'); + expect(element.all(by.css('#escaped-html a')).count()).toEqual(0); + }); + + it('should update', function() { + element(by.model('snippet')).clear(); + element(by.model('snippet')).sendKeys('new http://link.'); + expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()). + toBe('new http://link.'); + expect(element.all(by.css('#linky-filter a')).count()).toEqual(1); + expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()) + .toBe('new http://link.'); + }); + + it('should work with the target property', function() { + expect(element(by.id('linky-target')). + element(by.binding("snippetWithSingleURL | linky:'_blank'")).getText()). + toBe('http://angularjs.org/'); + expect(element(by.css('#linky-target a')).getAttribute('target')).toEqual('_blank'); + }); + + it('should optionally add custom attributes', function() { + expect(element(by.id('linky-custom-attributes')). + element(by.binding("snippetWithSingleURL | linky:'_self':{rel: 'nofollow'}")).getText()). + toBe('http://angularjs.org/'); + expect(element(by.css('#linky-custom-attributes a')).getAttribute('rel')).toEqual('nofollow'); + }); + + + */ +angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) { + var LINKY_URL_REGEXP = + /((ftp|https?):\/\/|(www\.)|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"\u201d\u2019]/i, + MAILTO_REGEXP = /^mailto:/i; + + var linkyMinErr = angular.$$minErr('linky'); + var isDefined = angular.isDefined; + var isFunction = angular.isFunction; + var isObject = angular.isObject; + var isString = angular.isString; + + return function(text, target, attributes) { + if (text == null || text === '') return text; + if (!isString(text)) throw linkyMinErr('notstring', 'Expected string but received: {0}', text); + + var attributesFn = + isFunction(attributes) ? attributes : + isObject(attributes) ? function getAttributesObject() {return attributes;} : + function getEmptyAttributesObject() {return {};}; + + var match; + var raw = text; + var html = []; + var url; + var i; + while ((match = raw.match(LINKY_URL_REGEXP))) { + // We can not end in these as they are sometimes found at the end of the sentence + url = match[0]; + // if we did not match ftp/http/www/mailto then assume mailto + if (!match[2] && !match[4]) { + url = (match[3] ? 'http://' : 'mailto:') + url; + } + i = match.index; + addText(raw.substr(0, i)); + addLink(url, match[0].replace(MAILTO_REGEXP, '')); + raw = raw.substring(i + match[0].length); + } + addText(raw); + return $sanitize(html.join('')); + + function addText(text) { + if (!text) { + return; + } + html.push(sanitizeText(text)); + } + + function addLink(url, text) { + var key, linkAttributes = attributesFn(url); + html.push(''); + addText(text); + html.push(''); + } + }; +}]); + + +})(window, window.angular); + +/** + * angular-timer - v1.3.4 - 2016-05-01 9:52 PM + * https://github.com/siddii/angular-timer + * + * Copyright (c) 2016 Siddique Hameed + * Licensed MIT + */ +var timerModule = angular.module('timer', []) + .directive('timer', ['$compile', function ($compile) { + return { + restrict: 'EA', + replace: false, + scope: { + interval: '=interval', + startTimeAttr: '=startTime', + endTimeAttr: '=endTime', + countdownattr: '=countdown', + finishCallback: '&finishCallback', + autoStart: '&autoStart', + language: '@?', + fallback: '@?', + maxTimeUnit: '=', + seconds: '=?', + minutes: '=?', + hours: '=?', + days: '=?', + months: '=?', + years: '=?', + secondsS: '=?', + minutesS: '=?', + hoursS: '=?', + daysS: '=?', + monthsS: '=?', + yearsS: '=?' + }, + controller: ['$scope', '$element', '$attrs', '$timeout', 'I18nService', '$interpolate', 'progressBarService', function ($scope, $element, $attrs, $timeout, I18nService, $interpolate, progressBarService) { + + // Checking for trim function since IE8 doesn't have it + // If not a function, create tirm with RegEx to mimic native trim + if (typeof String.prototype.trim !== 'function') { + String.prototype.trim = function () { + return this.replace(/^\s+|\s+$/g, ''); + }; + } + + //angular 1.2 doesn't support attributes ending in "-start", so we're + //supporting both "autostart" and "auto-start" as a solution for + //backward and forward compatibility. + $scope.autoStart = $attrs.autoStart || $attrs.autostart; + + + $scope.language = $scope.language || 'en'; + $scope.fallback = $scope.fallback || 'en'; + + //allow to change the language of the directive while already launched + $scope.$watch('language', function(newVal, oldVal) { + if(newVal !== undefined) { + i18nService.init(newVal, $scope.fallback); + } + }); + + //init momentJS i18n, default english + var i18nService = new I18nService(); + i18nService.init($scope.language, $scope.fallback); + + //progress bar + $scope.displayProgressBar = 0; + $scope.displayProgressActive = 'active'; //Bootstrap active effect for progress bar + + if ($element.html().trim().length === 0) { + $element.append($compile('' + $interpolate.startSymbol() + 'millis' + $interpolate.endSymbol() + '')($scope)); + } else { + $element.append($compile($element.contents())($scope)); + } + + $scope.startTime = null; + $scope.endTime = null; + $scope.timeoutId = null; + $scope.countdown = angular.isNumber($scope.countdownattr) && parseInt($scope.countdownattr, 10) >= 0 ? parseInt($scope.countdownattr, 10) : undefined; + $scope.isRunning = false; + + $scope.$on('timer-start', function () { + $scope.start(); + }); + + $scope.$on('timer-resume', function () { + $scope.resume(); + }); + + $scope.$on('timer-stop', function () { + $scope.stop(); + }); + + $scope.$on('timer-clear', function () { + $scope.clear(); + }); + + $scope.$on('timer-reset', function () { + $scope.reset(); + }); + + $scope.$on('timer-set-countdown', function (e, countdown) { + $scope.countdown = countdown; + }); + + function resetTimeout() { + if ($scope.timeoutId) { + clearTimeout($scope.timeoutId); + } + } + + $scope.$watch('startTimeAttr', function(newValue, oldValue) { + if (newValue !== oldValue && $scope.isRunning) { + $scope.start(); + } + }); + + $scope.$watch('endTimeAttr', function(newValue, oldValue) { + if (newValue !== oldValue && $scope.isRunning) { + $scope.start(); + } + }); + + $scope.start = $element[0].start = function () { + $scope.startTime = $scope.startTimeAttr ? moment($scope.startTimeAttr) : moment(); + $scope.endTime = $scope.endTimeAttr ? moment($scope.endTimeAttr) : null; + if (!angular.isNumber($scope.countdown)) { + $scope.countdown = angular.isNumber($scope.countdownattr) && parseInt($scope.countdownattr, 10) > 0 ? parseInt($scope.countdownattr, 10) : undefined; + } + resetTimeout(); + tick(); + $scope.isRunning = true; + }; + + $scope.resume = $element[0].resume = function () { + resetTimeout(); + if ($scope.countdownattr) { + $scope.countdown += 1; + } + $scope.startTime = moment().diff((moment($scope.stoppedTime).diff(moment($scope.startTime)))); + tick(); + $scope.isRunning = true; + }; + + $scope.stop = $scope.pause = $element[0].stop = $element[0].pause = function () { + var timeoutId = $scope.timeoutId; + $scope.clear(); + $scope.$emit('timer-stopped', {timeoutId: timeoutId, millis: $scope.millis, seconds: $scope.seconds, minutes: $scope.minutes, hours: $scope.hours, days: $scope.days}); + }; + + $scope.clear = $element[0].clear = function () { + // same as stop but without the event being triggered + $scope.stoppedTime = moment(); + resetTimeout(); + $scope.timeoutId = null; + $scope.isRunning = false; + }; + + $scope.reset = $element[0].reset = function () { + $scope.startTime = $scope.startTimeAttr ? moment($scope.startTimeAttr) : moment(); + $scope.endTime = $scope.endTimeAttr ? moment($scope.endTimeAttr) : null; + $scope.countdown = angular.isNumber($scope.countdownattr) && parseInt($scope.countdownattr, 10) > 0 ? parseInt($scope.countdownattr, 10) : undefined; + resetTimeout(); + tick(); + $scope.isRunning = false; + $scope.clear(); + }; + + $element.bind('$destroy', function () { + resetTimeout(); + $scope.isRunning = false; + }); + + + function calculateTimeUnits() { + var timeUnits = {}; //will contains time with units + + if ($attrs.startTime !== undefined){ + $scope.millis = moment().diff(moment($scope.startTimeAttr)); + } + + timeUnits = i18nService.getTimeUnits($scope.millis); + + // compute time values based on maxTimeUnit specification + if (!$scope.maxTimeUnit || $scope.maxTimeUnit === 'day') { + $scope.seconds = Math.floor(($scope.millis / 1000) % 60); + $scope.minutes = Math.floor((($scope.millis / (60000)) % 60)); + $scope.hours = Math.floor((($scope.millis / (3600000)) % 24)); + $scope.days = Math.floor((($scope.millis / (3600000)) / 24)); + $scope.months = 0; + $scope.years = 0; + } else if ($scope.maxTimeUnit === 'second') { + $scope.seconds = Math.floor($scope.millis / 1000); + $scope.minutes = 0; + $scope.hours = 0; + $scope.days = 0; + $scope.months = 0; + $scope.years = 0; + } else if ($scope.maxTimeUnit === 'minute') { + $scope.seconds = Math.floor(($scope.millis / 1000) % 60); + $scope.minutes = Math.floor($scope.millis / 60000); + $scope.hours = 0; + $scope.days = 0; + $scope.months = 0; + $scope.years = 0; + } else if ($scope.maxTimeUnit === 'hour') { + $scope.seconds = Math.floor(($scope.millis / 1000) % 60); + $scope.minutes = Math.floor((($scope.millis / (60000)) % 60)); + $scope.hours = Math.floor($scope.millis / 3600000); + $scope.days = 0; + $scope.months = 0; + $scope.years = 0; + } else if ($scope.maxTimeUnit === 'month') { + $scope.seconds = Math.floor(($scope.millis / 1000) % 60); + $scope.minutes = Math.floor((($scope.millis / (60000)) % 60)); + $scope.hours = Math.floor((($scope.millis / (3600000)) % 24)); + $scope.days = Math.floor((($scope.millis / (3600000)) / 24) % 30); + $scope.months = Math.floor((($scope.millis / (3600000)) / 24) / 30); + $scope.years = 0; + } else if ($scope.maxTimeUnit === 'year') { + $scope.seconds = Math.floor(($scope.millis / 1000) % 60); + $scope.minutes = Math.floor((($scope.millis / (60000)) % 60)); + $scope.hours = Math.floor((($scope.millis / (3600000)) % 24)); + $scope.days = Math.floor((($scope.millis / (3600000)) / 24) % 30); + $scope.months = Math.floor((($scope.millis / (3600000)) / 24 / 30) % 12); + $scope.years = Math.floor(($scope.millis / (3600000)) / 24 / 365); + } + // plural - singular unit decision (old syntax, for backwards compatibility and English only, could be deprecated!) + $scope.secondsS = ($scope.seconds === 1) ? '' : 's'; + $scope.minutesS = ($scope.minutes === 1) ? '' : 's'; + $scope.hoursS = ($scope.hours === 1) ? '' : 's'; + $scope.daysS = ($scope.days === 1)? '' : 's'; + $scope.monthsS = ($scope.months === 1)? '' : 's'; + $scope.yearsS = ($scope.years === 1)? '' : 's'; + + + // new plural-singular unit decision functions (for custom units and multilingual support) + $scope.secondUnit = timeUnits.seconds; + $scope.minuteUnit = timeUnits.minutes; + $scope.hourUnit = timeUnits.hours; + $scope.dayUnit = timeUnits.days; + $scope.monthUnit = timeUnits.months; + $scope.yearUnit = timeUnits.years; + + //add leading zero if number is smaller than 10 + $scope.sseconds = $scope.seconds < 10 ? '0' + $scope.seconds : $scope.seconds; + $scope.mminutes = $scope.minutes < 10 ? '0' + $scope.minutes : $scope.minutes; + $scope.hhours = $scope.hours < 10 ? '0' + $scope.hours : $scope.hours; + $scope.ddays = $scope.days < 10 ? '0' + $scope.days : $scope.days; + $scope.mmonths = $scope.months < 10 ? '0' + $scope.months : $scope.months; + $scope.yyears = $scope.years < 10 ? '0' + $scope.years : $scope.years; + + } + + //determine initial values of time units and add AddSeconds functionality + if ($scope.countdownattr) { + $scope.millis = $scope.countdownattr * 1000; + + $scope.addCDSeconds = $element[0].addCDSeconds = function (extraSeconds) { + $scope.countdown += extraSeconds; + $scope.$digest(); + if (!$scope.isRunning) { + $scope.start(); + } + }; + + $scope.$on('timer-add-cd-seconds', function (e, extraSeconds) { + $timeout(function () { + $scope.addCDSeconds(extraSeconds); + }); + }); + + $scope.$on('timer-set-countdown-seconds', function (e, countdownSeconds) { + if (!$scope.isRunning) { + $scope.clear(); + } + + $scope.countdown = countdownSeconds; + $scope.millis = countdownSeconds * 1000; + calculateTimeUnits(); + }); + } else { + $scope.millis = 0; + } + calculateTimeUnits(); + + var tick = function tick() { + var typeTimer = null; // countdown or endTimeAttr + $scope.millis = moment().diff($scope.startTime); + var adjustment = $scope.millis % 1000; + + if ($scope.endTimeAttr) { + typeTimer = $scope.endTimeAttr; + $scope.millis = moment($scope.endTime).diff(moment()); + adjustment = $scope.interval - $scope.millis % 1000; + } + + if ($scope.countdownattr) { + typeTimer = $scope.countdownattr; + $scope.millis = $scope.countdown * 1000; + } + + if ($scope.millis < 0) { + $scope.stop(); + $scope.millis = 0; + calculateTimeUnits(); + if($scope.finishCallback) { + $scope.$eval($scope.finishCallback); + } + return; + } + calculateTimeUnits(); + + //We are not using $timeout for a reason. Please read here - https://github.com/siddii/angular-timer/pull/5 + $scope.timeoutId = setTimeout(function () { + tick(); + $scope.$digest(); + }, $scope.interval - adjustment); + + $scope.$emit('timer-tick', {timeoutId: $scope.timeoutId, millis: $scope.millis, timerElement: $element[0]}); + + if ($scope.countdown > 0) { + $scope.countdown--; + } + else if ($scope.countdown <= 0) { + $scope.stop(); + if($scope.finishCallback) { + $scope.$eval($scope.finishCallback); + } + } + + if(typeTimer !== null){ + //calculate progress bar + $scope.progressBar = progressBarService.calculateProgressBar($scope.startTime, $scope.millis, $scope.endTime, $scope.countdownattr); + + if($scope.progressBar === 100){ + $scope.displayProgressActive = ''; //No more Bootstrap active effect + } + } + }; + + if ($scope.autoStart === undefined || $scope.autoStart === true) { + $scope.start(); + } + }] + }; + }]); + +/* commonjs package manager support (eg componentjs) */ +if (typeof module !== "undefined" && typeof exports !== "undefined" && module.exports === exports){ + module.exports = timerModule; +} + +var app = angular.module('timer'); + +app.factory('I18nService', function() { + + var I18nService = function() {}; + + I18nService.prototype.language = 'en'; + I18nService.prototype.fallback = 'en'; + I18nService.prototype.timeHumanizer = {}; + + I18nService.prototype.init = function init(lang, fallback) { + var supported_languages = humanizeDuration.getSupportedLanguages(); + + this.fallback = (fallback !== undefined) ? fallback : 'en'; + if (supported_languages.indexOf(fallback) === -1) { + this.fallback = 'en'; + } + + this.language = lang; + if (supported_languages.indexOf(lang) === -1) { + this.language = this.fallback; + } + + //moment init + moment.locale(this.language); //@TODO maybe to remove, it should be handle by the user's application itself, and not inside the directive + + //human duration init, using it because momentjs does not allow accurate time ( + // momentJS: a few moment ago, human duration : 4 seconds ago + this.timeHumanizer = humanizeDuration.humanizer({ + language: this.language, + halfUnit:false + }); + }; + + /** + * get time with units from momentJS i18n + * @param {int} millis + * @returns {{millis: string, seconds: string, minutes: string, hours: string, days: string, months: string, years: string}} + */ + I18nService.prototype.getTimeUnits = function getTimeUnits(millis) { + var diffFromAlarm = Math.round(millis/1000) * 1000; //time in milliseconds, get rid of the last 3 ms value to avoid 2.12 seconds display + + var time = {}; + + if (typeof this.timeHumanizer != 'undefined'){ + time = { + 'millis' : this.timeHumanizer(diffFromAlarm, { units: ["milliseconds"] }), + 'seconds' : this.timeHumanizer(diffFromAlarm, { units: ["seconds"] }), + 'minutes' : this.timeHumanizer(diffFromAlarm, { units: ["minutes", "seconds"] }) , + 'hours' : this.timeHumanizer(diffFromAlarm, { units: ["hours", "minutes", "seconds"] }) , + 'days' : this.timeHumanizer(diffFromAlarm, { units: ["days", "hours", "minutes", "seconds"] }) , + 'months' : this.timeHumanizer(diffFromAlarm, { units: ["months", "days", "hours", "minutes", "seconds"] }) , + 'years' : this.timeHumanizer(diffFromAlarm, { units: ["years", "months", "days", "hours", "minutes", "seconds"] }) + }; + } + else { + console.error('i18nService has not been initialized. You must call i18nService.init("en") for example'); + } + + return time; + }; + + return I18nService; +}); + +var app = angular.module('timer'); + +app.factory('progressBarService', function() { + + var ProgressBarService = function() {}; + + /** + * calculate the remaining time in a progress bar in percentage + * @param {momentjs} startValue in seconds + * @param {integer} currentCountdown, where are we in the countdown + * @param {integer} remainingTime, remaining milliseconds + * @param {integer} endTime, end time, can be undefined + * @param {integer} coutdown, original coutdown value, can be undefined + * + * joke : https://www.youtube.com/watch?v=gENVB6tjq_M + * @return {float} 0 --> 100 + */ + ProgressBarService.prototype.calculateProgressBar = function calculateProgressBar(startValue, remainingTime, endTimeAttr, coutdown) { + var displayProgressBar = 0, + endTimeValue, + initialCountdown; + + remainingTime = remainingTime / 1000; //seconds + + + if(endTimeAttr !== null){ + endTimeValue = moment(endTimeAttr); + initialCountdown = endTimeValue.diff(startValue, 'seconds'); + displayProgressBar = remainingTime * 100 / initialCountdown; + } + else { + displayProgressBar = remainingTime * 100 / coutdown; + } + + displayProgressBar = 100 - displayProgressBar; //To have 0 to 100 and not 100 to 0 + displayProgressBar = Math.round(displayProgressBar * 10) / 10; //learn more why : http://stackoverflow.com/questions/588004/is-floating-point-math-broken + + if(displayProgressBar > 100){ //security + displayProgressBar = 100; + } + + return displayProgressBar; + }; + + return new ProgressBarService(); +}); + +/*! + * angular-toasty + */ + +'use strict'; + +/** + * + * The MIT License (MIT) + * + * Copyright (c) 2015 Invertase + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +angular.module('angular-toasty', []); +angular.module('angular-toasty').directive('toasty', ['toasty', '$timeout', '$sce', function(toasty, $timeout, $sce) { + return { + replace: true, + restrict: 'EA', + scope: true, + link: function(scope, element, attrs) { + + // Init the counter + var uniqueCounter = 0; + + // Allowed themes + var themes = ['default', 'material', 'bootstrap']; + + // Init the position + scope.position = ''; + + // Init the toasty store + scope.toasty = []; + + // On new rootScope toasty-new broadcast + scope.$on('toasty-new', function(event, data) { + var config = data.config; + var options = data.options; + + if (!scope.position) + scope.position = 'toasty-position-' + config.position; + + add(config, options); + }); + + // On new rootScope toasty-clear broadcast + scope.$on('toasty-clear', function(event, data) { + clear(data.id); + }); + + // On ng-click="close", remove the specific toast + scope.close = function(id) { + clear(id); + }; + + // On ng-click="close", remove the specific toast + scope.clickToasty = function(toast) { + scope.$broadcast('toasty-clicked', toast); + if (toast.onClick && angular.isFunction(toast.onClick)) + toast.onClick.call(toast); + if (toast.clickToClose) + clear(toast.id); + }; + + // Clear all, or indivudally toast + function clear(id) { + if (!id) { + angular.forEach(scope.toasty, function(value, key) { + if (value.onRemove && angular.isFunction(value.onRemove)) + value.onRemove.call(scope.toasty[key]); + }); + scope.toasty = []; + scope.$broadcast('toasty-cleared'); + } else + angular.forEach(scope.toasty, function(value, key) { + if (value.id == id) { + scope.$broadcast('toasty-cleared', scope.toasty[key]); + if (value.onRemove && angular.isFunction(value.onRemove)) + value.onRemove.call(scope.toasty[key]); + scope.toasty.splice(key, 1); + if(!scope.$$phase) + scope.$digest(); + } + }); + } + + // Custom setTimeout function for specific + // setTimeouts on individual toasts + function setTimeout(toasty, time) { + toasty.timeout = $timeout(function() { + clear(toasty.id); + }, time); + } + + // Checks whether the local option is set, if not, + // checks the global config + function checkConfigItem(config, options, property) { + if (options[property] == false) return false; + else if (!options[property]) return config[property]; + else return true; + } + + // Add a new toast item + function add(config, options) { + // Set a unique counter for an id + uniqueCounter++; + + // Set the local vs global config items + var sound = checkConfigItem(config, options, 'sound'); + var showClose = checkConfigItem(config, options, 'showClose'); + var clickToClose = checkConfigItem(config, options, 'clickToClose'); + var html = checkConfigItem(config, options, 'html'); + var shake = checkConfigItem(config, options, 'shake'); + + // If we have a theme set, make sure it's a valid one + var theme; + if (options.theme) + theme = themes.indexOf(options.theme) > -1 ? options.theme : config.theme; + else + theme = config.theme; + + // If we've gone over our limit, remove the earliest + // one from the array + if (scope.toasty.length >= config.limit) + scope.toasty.shift(); + + // If sound is enabled, play the audio tag + if (sound) + document.getElementById('toasty-sound').play(); + + var toast = { + id: uniqueCounter, + title: html ? $sce.trustAsHtml(options.title) : options.title, + msg: html ? $sce.trustAsHtml(options.msg) : options.msg, + showClose: showClose, + clickToClose: clickToClose, + sound: sound, + shake: shake ? 'toasty-shake' : '', + html: html, + type: 'toasty-type-' + options.type, + theme: 'toasty-theme-' + theme, + onAdd: options.onAdd && angular.isFunction(options.onAdd) ? options.onAdd : null, + onRemove: options.onRemove && angular.isFunction(options.onRemove) ? options.onRemove : null, + onClick: options.onClick && angular.isFunction(options.onClick) ? options.onClick : null + }; + + // Push up a new toast item + scope.toasty.push(toast); + + // If we have a onAdd function, call it here + if (options.onAdd && angular.isFunction(options.onAdd)) + options.onAdd.call(toast); + + // Broadcast that the toasty was added + scope.$broadcast('toasty-added', toast); + + // If there's a timeout individually or globally, + // set the toast to timeout + if (options.timeout != false) { + if (options.timeout || config.timeout) + setTimeout(scope.toasty[scope.toasty.length - 1], options.timeout || config.timeout); + } + + } + }, + template: '
      ' + + '' + + '
      ' + + '
      ' + + '
      ' + + '' + + '' + + '
      ' + + '' + + '' + + '
      ' + +'
      ' + + '
      ' + } +}]); +angular.module('angular-toasty').provider('toastyConfig', function() { + + /** + * Default global config + * @type {Object} + */ + var object = { + limit: 5, + showClose: true, + clickToClose: false, + position: 'bottom-right', + timeout: 5000, + sound: true, + html: false, + shake: false, + theme: 'default' + }; + + /** + * Over-ride config + * @type {Object} + */ + var updated = {}; + + return { + setConfig: function(override) { + updated = override; + }, + $get: function() { + return { + config: angular.extend(object, updated) + } + } + } +}); +angular.module('angular-toasty').factory('toasty', ['$rootScope', 'toastyConfig', function($rootScope, toastyConfig) { + + // Get the global config + var config = toastyConfig.config; + + /** + * Broadcast a new toasty item to the rootscope + * @param {object} options Individual toasty config overrides + * @param {string} type Type of toasty; success, info, error etc. + */ + var toasty = function(options, type) { + + if (angular.isString(options) && options != '' || angular.isNumber(options)) { + options = { + title: options.toString() + }; + } + + if (!options || !options.title && !options.msg) { + console.error('angular-toasty: No toast title or message specified!'); + } else { + options.type = type || 'default'; + $rootScope.$broadcast('toasty-new', {config: config, options: options}); + } + }; + + /** + * Toasty types + */ + + toasty.default = function(options) { + toasty(options); + }; + + toasty.info = function(options) { + toasty(options, 'info'); + }; + + toasty.wait = function(options) { + toasty(options, 'wait'); + }; + + toasty.success = function(options) { + toasty(options, 'success'); + }; + + toasty.error = function(options) { + toasty(options, 'error'); + }; + + toasty.warning = function(options) { + toasty(options, 'warning'); + }; + + /** + * Broadcast a clear event + * @param {int} Optional ID of the toasty to clear + */ + + toasty.clear = function(id) { + $rootScope.$broadcast('toasty-clear', { id: id }); + }; + + /** + * Return the global config + */ + + toasty.getGlobalConfig = function() { + return config; + }; + + return toasty; + +}]); +/*! + * angular-translate - v2.11.0 - 2016-03-20 + * + * 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.$translatePartialLoaderProvider + * + * @description + * By using a $translatePartialLoaderProvider you can configure a list of a needed + * translation parts directly during the configuration phase of your application's + * lifetime. All parts you add by using this provider would be loaded by + * angular-translate at the startup as soon as possible. + */ + .provider('$translatePartialLoader', $translatePartialLoader); + +function $translatePartialLoader() { + + 'use strict'; + + /** + * @constructor + * @name Part + * + * @description + * Represents Part object to add and set parts at runtime. + */ + function Part(name, priority) { + this.name = name; + this.isActive = true; + this.tables = {}; + this.priority = priority || 0; + } + + /** + * @name parseUrl + * @method + * + * @description + * Returns a parsed url template string and replaces given target lang + * and part name it. + * + * @param {string|function} urlTemplate - Either a string containing an url pattern (with + * '{part}' and '{lang}') or a function(part, lang) + * returning a string. + * @param {string} targetLang - Language key for language to be used. + * @return {string} Parsed url template string + */ + Part.prototype.parseUrl = function(urlTemplate, targetLang) { + if (angular.isFunction(urlTemplate)) { + return urlTemplate(this.name, targetLang); + } + return urlTemplate.replace(/\{part\}/g, this.name).replace(/\{lang\}/g, targetLang); + }; + + Part.prototype.getTable = function(lang, $q, $http, $httpOptions, urlTemplate, errorHandler) { + + if (!this.tables[lang]) { + var self = this; + + return $http(angular.extend({ + method : 'GET', + url: this.parseUrl(urlTemplate, lang) + }, $httpOptions)) + .then(function(result){ + self.tables[lang] = result.data; + return result.data; + }, function() { + if (errorHandler) { + return errorHandler(self.name, lang) + .then(function(data) { + self.tables[lang] = data; + return data; + }, function() { + return $q.reject(self.name); + }); + } else { + return $q.reject(self.name); + } + }); + + } else { + return $q.when(this.tables[lang]); + } + }; + + var parts = {}; + + function hasPart(name) { + return Object.prototype.hasOwnProperty.call(parts, name); + } + + function isStringValid(str) { + return angular.isString(str) && str !== ''; + } + + function isPartAvailable(name) { + if (!isStringValid(name)) { + throw new TypeError('Invalid type of a first argument, a non-empty string expected.'); + } + + return (hasPart(name) && parts[name].isActive); + } + + function deepExtend(dst, src) { + for (var property in src) { + if (src[property] && src[property].constructor && + src[property].constructor === Object) { + dst[property] = dst[property] || {}; + deepExtend(dst[property], src[property]); + } else { + dst[property] = src[property]; + } + } + return dst; + } + + function getPrioritizedParts() { + var prioritizedParts = []; + for(var part in parts) { + if (parts[part].isActive) { + prioritizedParts.push(parts[part]); + } + } + prioritizedParts.sort(function (a, b) { + return a.priority - b.priority; + }); + return prioritizedParts; + } + + + /** + * @ngdoc function + * @name pascalprecht.translate.$translatePartialLoaderProvider#addPart + * @methodOf pascalprecht.translate.$translatePartialLoaderProvider + * + * @description + * Registers a new part of the translation table to be loaded once the + * `angular-translate` gets into runtime phase. It does not actually load any + * translation data, but only registers a part to be loaded in the future. + * + * @param {string} name A name of the part to add + * @param {int} [priority=0] Sets the load priority of this part. + * + * @returns {object} $translatePartialLoaderProvider, so this method is chainable + * @throws {TypeError} The method could throw a **TypeError** if you pass the param + * of the wrong type. Please, note that the `name` param has to be a + * non-empty **string**. + */ + this.addPart = function(name, priority) { + if (!isStringValid(name)) { + throw new TypeError('Couldn\'t add part, part name has to be a string!'); + } + + if (!hasPart(name)) { + parts[name] = new Part(name, priority); + } + parts[name].isActive = true; + + return this; + }; + + /** + * @ngdocs function + * @name pascalprecht.translate.$translatePartialLoaderProvider#setPart + * @methodOf pascalprecht.translate.$translatePartialLoaderProvider + * + * @description + * Sets a translation table to the specified part. This method does not make the + * specified part available, but only avoids loading this part from the server. + * + * @param {string} lang A language of the given translation table + * @param {string} part A name of the target part + * @param {object} table A translation table to set to the specified part + * + * @return {object} $translatePartialLoaderProvider, so this method is chainable + * @throws {TypeError} The method could throw a **TypeError** if you pass params + * of the wrong type. Please, note that the `lang` and `part` params have to be a + * non-empty **string**s and the `table` param has to be an object. + */ + this.setPart = function (lang, part, table) { + if (!isStringValid(lang)) { + throw new TypeError('Couldn\'t set part.`lang` parameter has to be a string!'); + } + if (!isStringValid(part)) { + throw new TypeError('Couldn\'t set part.`part` parameter has to be a string!'); + } + if (typeof table !== 'object' || table === null) { + throw new TypeError('Couldn\'t set part. `table` parameter has to be an object!'); + } + + if (!hasPart(part)) { + parts[part] = new Part(part); + parts[part].isActive = false; + } + + parts[part].tables[lang] = table; + return this; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translatePartialLoaderProvider#deletePart + * @methodOf pascalprecht.translate.$translatePartialLoaderProvider + * + * @description + * Removes the previously added part of the translation data. So, `angular-translate` will not + * load it at the startup. + * + * @param {string} name A name of the part to delete + * + * @returns {object} $translatePartialLoaderProvider, so this method is chainable + * + * @throws {TypeError} The method could throw a **TypeError** if you pass the param of the wrong + * type. Please, note that the `name` param has to be a non-empty **string**. + */ + this.deletePart = function (name) { + if (!isStringValid(name)) { + throw new TypeError('Couldn\'t delete part, first arg has to be string.'); + } + + if (hasPart(name)) { + parts[name].isActive = false; + } + + return this; + }; + + + /** + * @ngdoc function + * @name pascalprecht.translate.$translatePartialLoaderProvider#isPartAvailable + * @methodOf pascalprecht.translate.$translatePartialLoaderProvider + * + * @description + * Checks if the specific part is available. A part becomes available after it was added by the + * `addPart` method. Available parts would be loaded from the server once the `angular-translate` + * asks the loader to that. + * + * @param {string} name A name of the part to check + * + * @returns {boolean} Returns **true** if the part is available now and **false** if not. + * + * @throws {TypeError} The method could throw a **TypeError** if you pass the param of the wrong + * type. Please, note that the `name` param has to be a non-empty **string**. + */ + this.isPartAvailable = isPartAvailable; + + /** + * @ngdoc object + * @name pascalprecht.translate.$translatePartialLoader + * + * @requires $q + * @requires $http + * @requires $injector + * @requires $rootScope + * @requires $translate + * + * @description + * + * @param {object} options Options object + * + * @throws {TypeError} + */ + this.$get = ['$rootScope', '$injector', '$q', '$http', + function($rootScope, $injector, $q, $http) { + + /** + * @ngdoc event + * @name pascalprecht.translate.$translatePartialLoader#$translatePartialLoaderStructureChanged + * @eventOf pascalprecht.translate.$translatePartialLoader + * @eventType broadcast on root scope + * + * @description + * A $translatePartialLoaderStructureChanged event is called when a state of the loader was + * changed somehow. It could mean either some part is added or some part is deleted. Anyway when + * you get this event the translation table is not longer current and has to be updated. + * + * @param {string} name A name of the part which is a reason why the event was fired + */ + + var service = function(options) { + if (!isStringValid(options.key)) { + throw new TypeError('Unable to load data, a key is not a non-empty string.'); + } + + if (!isStringValid(options.urlTemplate) && !angular.isFunction(options.urlTemplate)) { + throw new TypeError('Unable to load data, a urlTemplate is not a non-empty string or not a function.'); + } + + var errorHandler = options.loadFailureHandler; + if (errorHandler !== undefined) { + if (!angular.isString(errorHandler)) { + throw new Error('Unable to load data, a loadFailureHandler is not a string.'); + } else { + errorHandler = $injector.get(errorHandler); + } + } + + var loaders = [], + prioritizedParts = getPrioritizedParts(); + + angular.forEach(prioritizedParts, function(part) { + loaders.push( + part.getTable(options.key, $q, $http, options.$http, options.urlTemplate, errorHandler) + ); + part.urlTemplate = options.urlTemplate; + }); + + return $q.all(loaders) + .then(function() { + var table = {}; + prioritizedParts = getPrioritizedParts(); + angular.forEach(prioritizedParts, function(part) { + deepExtend(table, part.tables[options.key]); + }); + return table; + }, function() { + return $q.reject(options.key); + }); + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translatePartialLoader#addPart + * @methodOf pascalprecht.translate.$translatePartialLoader + * + * @description + * Registers a new part of the translation table. This method does not actually perform any xhr + * requests to get translation data. The new parts will be loaded in order of priority from the server next time + * `angular-translate` asks the loader to load translations. + * + * @param {string} name A name of the part to add + * @param {int} [priority=0] Sets the load priority of this part. + * + * @returns {object} $translatePartialLoader, so this method is chainable + * + * @fires {$translatePartialLoaderStructureChanged} The $translatePartialLoaderStructureChanged + * event would be fired by this method in case the new part affected somehow on the loaders + * state. This way it means that there are a new translation data available to be loaded from + * the server. + * + * @throws {TypeError} The method could throw a **TypeError** if you pass the param of the wrong + * type. Please, note that the `name` param has to be a non-empty **string**. + */ + service.addPart = function(name, priority) { + if (!isStringValid(name)) { + throw new TypeError('Couldn\'t add part, first arg has to be a string'); + } + + if (!hasPart(name)) { + parts[name] = new Part(name, priority); + $rootScope.$emit('$translatePartialLoaderStructureChanged', name); + } else if (!parts[name].isActive) { + parts[name].isActive = true; + $rootScope.$emit('$translatePartialLoaderStructureChanged', name); + } + + return service; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translatePartialLoader#deletePart + * @methodOf pascalprecht.translate.$translatePartialLoader + * + * @description + * Deletes the previously added part of the translation data. The target part could be deleted + * either logically or physically. When the data is deleted logically it is not actually deleted + * from the browser, but the loader marks it as not active and prevents it from affecting on the + * translations. If the deleted in such way part is added again, the loader will use the + * previously loaded data rather than loading it from the server once more time. But if the data + * is deleted physically, the loader will completely remove all information about it. So in case + * of recycling this part will be loaded from the server again. + * + * @param {string} name A name of the part to delete + * @param {boolean=} [removeData=false] An indicator if the loader has to remove a loaded + * translation data physically. If the `removeData` if set to **false** the loaded data will not be + * deleted physically and might be reused in the future to prevent an additional xhr requests. + * + * @returns {object} $translatePartialLoader, so this method is chainable + * + * @fires {$translatePartialLoaderStructureChanged} The $translatePartialLoaderStructureChanged + * event would be fired by this method in case a part deletion process affects somehow on the + * loaders state. This way it means that some part of the translation data is now deprecated and + * the translation table has to be recompiled with the remaining translation parts. + * + * @throws {TypeError} The method could throw a **TypeError** if you pass some param of the + * wrong type. Please, note that the `name` param has to be a non-empty **string** and + * the `removeData` param has to be either **undefined** or **boolean**. + */ + service.deletePart = function(name, removeData) { + if (!isStringValid(name)) { + throw new TypeError('Couldn\'t delete part, first arg has to be string'); + } + + if (removeData === undefined) { + removeData = false; + } else if (typeof removeData !== 'boolean') { + throw new TypeError('Invalid type of a second argument, a boolean expected.'); + } + + if (hasPart(name)) { + var wasActive = parts[name].isActive; + if (removeData) { + var $translate = $injector.get('$translate'); + var cache = $translate.loaderCache(); + if (typeof(cache) === 'string') { + // getting on-demand instance of loader + cache = $injector.get(cache); + } + // Purging items from cache... + if (typeof(cache) === 'object') { + angular.forEach(parts[name].tables, function(value, key) { + cache.remove(parts[name].parseUrl(parts[name].urlTemplate, key)); + }); + } + delete parts[name]; + } else { + parts[name].isActive = false; + } + if (wasActive) { + $rootScope.$emit('$translatePartialLoaderStructureChanged', name); + } + } + + return service; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translatePartialLoader#isPartLoaded + * @methodOf pascalprecht.translate.$translatePartialLoader + * + * @description + * Checks if the registered translation part is loaded into the translation table. + * + * @param {string} name A name of the part + * @param {string} lang A key of the language + * + * @returns {boolean} Returns **true** if the translation of the part is loaded to the translation table and **false** if not. + * + * @throws {TypeError} The method could throw a **TypeError** if you pass the param of the wrong + * type. Please, note that the `name` and `lang` params have to be non-empty **string**. + */ + service.isPartLoaded = function(name, lang) { + return angular.isDefined(parts[name]) && angular.isDefined(parts[name].tables[lang]); + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translatePartialLoader#getRegisteredParts + * @methodOf pascalprecht.translate.$translatePartialLoader + * + * @description + * Gets names of the parts that were added with the `addPart`. + * + * @returns {array} Returns array of registered parts, if none were registered then an empty array is returned. + */ + service.getRegisteredParts = function() { + var registeredParts = []; + angular.forEach(parts, function(p){ + if(p.isActive) { + registeredParts.push(p.name); + } + }); + return registeredParts; + }; + + + + /** + * @ngdoc function + * @name pascalprecht.translate.$translatePartialLoader#isPartAvailable + * @methodOf pascalprecht.translate.$translatePartialLoader + * + * @description + * Checks if a target translation part is available. The part becomes available just after it was + * added by the `addPart` method. Part's availability does not mean that it was loaded from the + * server, but only that it was added to the loader. The available part might be loaded next + * time the loader is called. + * + * @param {string} name A name of the part to delete + * + * @returns {boolean} Returns **true** if the part is available now and **false** if not. + * + * @throws {TypeError} The method could throw a **TypeError** if you pass the param of the wrong + * type. Please, note that the `name` param has to be a non-empty **string**. + */ + service.isPartAvailable = isPartAvailable; + + return service; + + }]; + +} + +$translatePartialLoader.displayName = '$translatePartialLoader'; +return 'pascalprecht.translate'; + +})); + +/*! + * angular-translate - v2.11.0 - 2016-03-20 + * + * 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 () { + +/** + * @ngdoc overview + * @name pascalprecht.translate + * + * @description + * The main module which holds everything together. + */ +runTranslate.$inject = ['$translate']; +$translate.$inject = ['$STORAGE_KEY', '$windowProvider', '$translateSanitizationProvider', 'pascalprechtTranslateOverrider']; +$translateDefaultInterpolation.$inject = ['$interpolate', '$translateSanitization']; +translateDirective.$inject = ['$translate', '$q', '$interpolate', '$compile', '$parse', '$rootScope']; +translateCloakDirective.$inject = ['$translate', '$rootScope']; +translateFilterFactory.$inject = ['$parse', '$translate']; +$translationCache.$inject = ['$cacheFactory']; +angular.module('pascalprecht.translate', ['ng']) + .run(runTranslate); + +function runTranslate($translate) { + + 'use strict'; + + var key = $translate.storageKey(), + storage = $translate.storage(); + + var fallbackFromIncorrectStorageValue = function () { + var preferred = $translate.preferredLanguage(); + if (angular.isString(preferred)) { + $translate.use(preferred); + // $translate.use() will also remember the language. + // So, we don't need to call storage.put() here. + } else { + storage.put(key, $translate.use()); + } + }; + + fallbackFromIncorrectStorageValue.displayName = 'fallbackFromIncorrectStorageValue'; + + if (storage) { + if (!storage.get(key)) { + fallbackFromIncorrectStorageValue(); + } else { + $translate.use(storage.get(key))['catch'](fallbackFromIncorrectStorageValue); + } + } else if (angular.isString($translate.preferredLanguage())) { + $translate.use($translate.preferredLanguage()); + } +} + +runTranslate.displayName = 'runTranslate'; + +/** + * @ngdoc object + * @name pascalprecht.translate.$translateSanitizationProvider + * + * @description + * + * Configurations for $translateSanitization + */ +angular.module('pascalprecht.translate').provider('$translateSanitization', $translateSanitizationProvider); + +function $translateSanitizationProvider () { + + 'use strict'; + + var $sanitize, + currentStrategy = null, // TODO change to either 'sanitize', 'escape' or ['sanitize', 'escapeParameters'] in 3.0. + hasConfiguredStrategy = false, + hasShownNoStrategyConfiguredWarning = false, + strategies; + + /** + * Definition of a sanitization strategy function + * @callback StrategyFunction + * @param {string|object} value - value to be sanitized (either a string or an interpolated value map) + * @param {string} mode - either 'text' for a string (translation) or 'params' for the interpolated params + * @return {string|object} + */ + + /** + * @ngdoc property + * @name strategies + * @propertyOf pascalprecht.translate.$translateSanitizationProvider + * + * @description + * Following strategies are built-in: + *
      + *
      sanitize
      + *
      Sanitizes HTML in the translation text using $sanitize
      + *
      escape
      + *
      Escapes HTML in the translation
      + *
      sanitizeParameters
      + *
      Sanitizes HTML in the values of the interpolation parameters using $sanitize
      + *
      escapeParameters
      + *
      Escapes HTML in the values of the interpolation parameters
      + *
      escaped
      + *
      Support legacy strategy name 'escaped' for backwards compatibility (will be removed in 3.0)
      + *
      + * + */ + + strategies = { + sanitize: function (value, mode) { + if (mode === 'text') { + value = htmlSanitizeValue(value); + } + return value; + }, + escape: function (value, mode) { + if (mode === 'text') { + value = htmlEscapeValue(value); + } + return value; + }, + sanitizeParameters: function (value, mode) { + if (mode === 'params') { + value = mapInterpolationParameters(value, htmlSanitizeValue); + } + return value; + }, + escapeParameters: function (value, mode) { + if (mode === 'params') { + value = mapInterpolationParameters(value, htmlEscapeValue); + } + return value; + } + }; + // Support legacy strategy name 'escaped' for backwards compatibility. + // TODO should be removed in 3.0 + strategies.escaped = strategies.escapeParameters; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateSanitizationProvider#addStrategy + * @methodOf pascalprecht.translate.$translateSanitizationProvider + * + * @description + * Adds a sanitization strategy to the list of known strategies. + * + * @param {string} strategyName - unique key for a strategy + * @param {StrategyFunction} strategyFunction - strategy function + * @returns {object} this + */ + this.addStrategy = function (strategyName, strategyFunction) { + strategies[strategyName] = strategyFunction; + return this; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateSanitizationProvider#removeStrategy + * @methodOf pascalprecht.translate.$translateSanitizationProvider + * + * @description + * Removes a sanitization strategy from the list of known strategies. + * + * @param {string} strategyName - unique key for a strategy + * @returns {object} this + */ + this.removeStrategy = function (strategyName) { + delete strategies[strategyName]; + return this; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateSanitizationProvider#useStrategy + * @methodOf pascalprecht.translate.$translateSanitizationProvider + * + * @description + * Selects a sanitization strategy. When an array is provided the strategies will be executed in order. + * + * @param {string|StrategyFunction|array} strategy The sanitization strategy / strategies which should be used. Either a name of an existing strategy, a custom strategy function, or an array consisting of multiple names and / or custom functions. + * @returns {object} this + */ + this.useStrategy = function (strategy) { + hasConfiguredStrategy = true; + currentStrategy = strategy; + return this; + }; + + /** + * @ngdoc object + * @name pascalprecht.translate.$translateSanitization + * @requires $injector + * @requires $log + * + * @description + * Sanitizes interpolation parameters and translated texts. + * + */ + this.$get = ['$injector', '$log', function ($injector, $log) { + + var cachedStrategyMap = {}; + + var applyStrategies = function (value, mode, selectedStrategies) { + angular.forEach(selectedStrategies, function (selectedStrategy) { + if (angular.isFunction(selectedStrategy)) { + value = selectedStrategy(value, mode); + } else if (angular.isFunction(strategies[selectedStrategy])) { + value = strategies[selectedStrategy](value, mode); + } else if (angular.isString(strategies[selectedStrategy])) { + if (!cachedStrategyMap[strategies[selectedStrategy]]) { + try { + cachedStrategyMap[strategies[selectedStrategy]] = $injector.get(strategies[selectedStrategy]); + } catch (e) { + cachedStrategyMap[strategies[selectedStrategy]] = function() {}; + throw new Error('pascalprecht.translate.$translateSanitization: Unknown sanitization strategy: \'' + selectedStrategy + '\''); + } + } + value = cachedStrategyMap[strategies[selectedStrategy]](value, mode); + } else { + throw new Error('pascalprecht.translate.$translateSanitization: Unknown sanitization strategy: \'' + selectedStrategy + '\''); + } + }); + return value; + }; + + // TODO: should be removed in 3.0 + var showNoStrategyConfiguredWarning = function () { + if (!hasConfiguredStrategy && !hasShownNoStrategyConfiguredWarning) { + $log.warn('pascalprecht.translate.$translateSanitization: No sanitization strategy has been configured. This can have serious security implications. See http://angular-translate.github.io/docs/#/guide/19_security for details.'); + hasShownNoStrategyConfiguredWarning = true; + } + }; + + if ($injector.has('$sanitize')) { + $sanitize = $injector.get('$sanitize'); + } + + return { + /** + * @ngdoc function + * @name pascalprecht.translate.$translateSanitization#useStrategy + * @methodOf pascalprecht.translate.$translateSanitization + * + * @description + * Selects a sanitization strategy. When an array is provided the strategies will be executed in order. + * + * @param {string|StrategyFunction|array} strategy The sanitization strategy / strategies which should be used. Either a name of an existing strategy, a custom strategy function, or an array consisting of multiple names and / or custom functions. + */ + useStrategy: (function (self) { + return function (strategy) { + self.useStrategy(strategy); + }; + })(this), + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateSanitization#sanitize + * @methodOf pascalprecht.translate.$translateSanitization + * + * @description + * Sanitizes a value. + * + * @param {string|object} value The value which should be sanitized. + * @param {string} mode The current sanitization mode, either 'params' or 'text'. + * @param {string|StrategyFunction|array} [strategy] Optional custom strategy which should be used instead of the currently selected strategy. + * @returns {string|object} sanitized value + */ + sanitize: function (value, mode, strategy) { + if (!currentStrategy) { + showNoStrategyConfiguredWarning(); + } + + if (arguments.length < 3) { + strategy = currentStrategy; + } + + if (!strategy) { + return value; + } + + var selectedStrategies = angular.isArray(strategy) ? strategy : [strategy]; + return applyStrategies(value, mode, selectedStrategies); + } + }; + }]; + + var htmlEscapeValue = function (value) { + var element = angular.element('
      '); + element.text(value); // not chainable, see #1044 + return element.html(); + }; + + var htmlSanitizeValue = function (value) { + if (!$sanitize) { + throw new Error('pascalprecht.translate.$translateSanitization: Error cannot find $sanitize service. Either include the ngSanitize module (https://docs.angularjs.org/api/ngSanitize) or use a sanitization strategy which does not depend on $sanitize, such as \'escape\'.'); + } + return $sanitize(value); + }; + + var mapInterpolationParameters = function (value, iteratee, stack) { + if (angular.isObject(value)) { + var result = angular.isArray(value) ? [] : {}; + + if (!stack) { + stack = []; + } else { + if (stack.indexOf(value) > -1) { + throw new Error('pascalprecht.translate.$translateSanitization: Error cannot interpolate parameter due recursive object'); + } + } + + stack.push(value); + angular.forEach(value, function (propertyValue, propertyKey) { + result[propertyKey] = mapInterpolationParameters(propertyValue, iteratee, stack); + }); + stack.splice(-1, 1); // remove last + + return result; + } else if (angular.isNumber(value)) { + return value; + } else { + return iteratee(value); + } + }; +} + +/** + * @ngdoc object + * @name pascalprecht.translate.$translateProvider + * @description + * + * $translateProvider allows developers to register translation-tables, asynchronous loaders + * and similar to configure translation behavior directly inside of a module. + * + */ +angular.module('pascalprecht.translate') +.constant('pascalprechtTranslateOverrider', {}) +.provider('$translate', $translate); + +function $translate($STORAGE_KEY, $windowProvider, $translateSanitizationProvider, pascalprechtTranslateOverrider) { + + 'use strict'; + + var $translationTable = {}, + $preferredLanguage, + $availableLanguageKeys = [], + $languageKeyAliases, + $fallbackLanguage, + $fallbackWasString, + $uses, + $nextLang, + $storageFactory, + $storageKey = $STORAGE_KEY, + $storagePrefix, + $missingTranslationHandlerFactory, + $interpolationFactory, + $interpolatorFactories = [], + $loaderFactory, + $cloakClassName = 'translate-cloak', + $loaderOptions, + $notFoundIndicatorLeft, + $notFoundIndicatorRight, + $postCompilingEnabled = false, + $forceAsyncReloadEnabled = false, + $nestedObjectDelimeter = '.', + $isReady = false, + loaderCache, + directivePriority = 0, + statefulFilter = true, + postProcessFn, + uniformLanguageTagResolver = 'default', + languageTagResolver = { + 'default': function (tag) { + return (tag || '').split('-').join('_'); + }, + java: function (tag) { + var temp = (tag || '').split('-').join('_'); + var parts = temp.split('_'); + return parts.length > 1 ? (parts[0].toLowerCase() + '_' + parts[1].toUpperCase()) : temp; + }, + bcp47: function (tag) { + var temp = (tag || '').split('_').join('-'); + var parts = temp.split('-'); + return parts.length > 1 ? (parts[0].toLowerCase() + '-' + parts[1].toUpperCase()) : temp; + }, + 'iso639-1': function (tag) { + var temp = (tag || '').split('_').join('-'); + var parts = temp.split('-'); + return parts[0].toLowerCase(); + } + }; + + var version = '2.11.0'; + + // tries to determine the browsers language + var getFirstBrowserLanguage = function () { + + // internal purpose only + if (angular.isFunction(pascalprechtTranslateOverrider.getLocale)) { + return pascalprechtTranslateOverrider.getLocale(); + } + + var nav = $windowProvider.$get().navigator, + browserLanguagePropertyKeys = ['language', 'browserLanguage', 'systemLanguage', 'userLanguage'], + i, + language; + + // support for HTML 5.1 "navigator.languages" + if (angular.isArray(nav.languages)) { + for (i = 0; i < nav.languages.length; i++) { + language = nav.languages[i]; + if (language && language.length) { + return language; + } + } + } + + // support for other well known properties in browsers + for (i = 0; i < browserLanguagePropertyKeys.length; i++) { + language = nav[browserLanguagePropertyKeys[i]]; + if (language && language.length) { + return language; + } + } + + return null; + }; + getFirstBrowserLanguage.displayName = 'angular-translate/service: getFirstBrowserLanguage'; + + // tries to determine the browsers locale + var getLocale = function () { + var locale = getFirstBrowserLanguage() || ''; + if (languageTagResolver[uniformLanguageTagResolver]) { + locale = languageTagResolver[uniformLanguageTagResolver](locale); + } + return locale; + }; + getLocale.displayName = 'angular-translate/service: getLocale'; + + /** + * @name indexOf + * @private + * + * @description + * indexOf polyfill. Kinda sorta. + * + * @param {array} array Array to search in. + * @param {string} searchElement Element to search for. + * + * @returns {int} Index of search element. + */ + var indexOf = function(array, searchElement) { + for (var i = 0, len = array.length; i < len; i++) { + if (array[i] === searchElement) { + return i; + } + } + return -1; + }; + + /** + * @name trim + * @private + * + * @description + * trim polyfill + * + * @returns {string} The string stripped of whitespace from both ends + */ + var trim = function() { + return this.toString().replace(/^\s+|\s+$/g, ''); + }; + + var negotiateLocale = function (preferred) { + if(!preferred) { + return; + } + + var avail = [], + locale = angular.lowercase(preferred), + i = 0, + n = $availableLanguageKeys.length; + + for (; i < n; i++) { + avail.push(angular.lowercase($availableLanguageKeys[i])); + } + + // Check for an exact match in our list of available keys + if (indexOf(avail, locale) > -1) { + return preferred; + } + + if ($languageKeyAliases) { + var alias; + for (var langKeyAlias in $languageKeyAliases) { + if ($languageKeyAliases.hasOwnProperty(langKeyAlias)) { + var hasWildcardKey = false; + var hasExactKey = Object.prototype.hasOwnProperty.call($languageKeyAliases, langKeyAlias) && + angular.lowercase(langKeyAlias) === angular.lowercase(preferred); + + if (langKeyAlias.slice(-1) === '*') { + hasWildcardKey = langKeyAlias.slice(0, -1) === preferred.slice(0, langKeyAlias.length - 1); + } + if (hasExactKey || hasWildcardKey) { + alias = $languageKeyAliases[langKeyAlias]; + if (indexOf(avail, angular.lowercase(alias)) > -1) { + return alias; + } + } + } + } + } + + // Check for a language code without region + var parts = preferred.split('_'); + + if (parts.length > 1 && indexOf(avail, angular.lowercase(parts[0])) > -1) { + return parts[0]; + } + + // If everything fails, return undefined. + return; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#translations + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * Registers a new translation table for specific language key. + * + * To register a translation table for specific language, pass a defined language + * key as first parameter. + * + *
      +   *  // register translation table for language: 'de_DE'
      +   *  $translateProvider.translations('de_DE', {
      +   *    'GREETING': 'Hallo Welt!'
      +   *  });
      +   *
      +   *  // register another one
      +   *  $translateProvider.translations('en_US', {
      +   *    'GREETING': 'Hello world!'
      +   *  });
      +   * 
      + * + * When registering multiple translation tables for for the same language key, + * the actual translation table gets extended. This allows you to define module + * specific translation which only get added, once a specific module is loaded in + * your app. + * + * Invoking this method with no arguments returns the translation table which was + * registered with no language key. Invoking it with a language key returns the + * related translation table. + * + * @param {string} langKey A language key. + * @param {object} translationTable A plain old JavaScript object that represents a translation table. + * + */ + var translations = function (langKey, translationTable) { + + if (!langKey && !translationTable) { + return $translationTable; + } + + if (langKey && !translationTable) { + if (angular.isString(langKey)) { + return $translationTable[langKey]; + } + } else { + if (!angular.isObject($translationTable[langKey])) { + $translationTable[langKey] = {}; + } + angular.extend($translationTable[langKey], flatObject(translationTable)); + } + return this; + }; + + this.translations = translations; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#cloakClassName + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * + * Let's you change the class name for `translate-cloak` directive. + * Default class name is `translate-cloak`. + * + * @param {string} name translate-cloak class name + */ + this.cloakClassName = function (name) { + if (!name) { + return $cloakClassName; + } + $cloakClassName = name; + return this; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#nestedObjectDelimeter + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * + * Let's you change the delimiter for namespaced translations. + * Default delimiter is `.`. + * + * @param {string} delimiter namespace separator + */ + this.nestedObjectDelimeter = function (delimiter) { + if (!delimiter) { + return $nestedObjectDelimeter; + } + $nestedObjectDelimeter = delimiter; + return this; + }; + + /** + * @name flatObject + * @private + * + * @description + * Flats an object. This function is used to flatten given translation data with + * namespaces, so they are later accessible via dot notation. + */ + var flatObject = function (data, path, result, prevKey) { + var key, keyWithPath, keyWithShortPath, val; + + if (!path) { + path = []; + } + if (!result) { + result = {}; + } + for (key in data) { + if (!Object.prototype.hasOwnProperty.call(data, key)) { + continue; + } + val = data[key]; + if (angular.isObject(val)) { + flatObject(val, path.concat(key), result, key); + } else { + keyWithPath = path.length ? ('' + path.join($nestedObjectDelimeter) + $nestedObjectDelimeter + key) : key; + if(path.length && key === prevKey){ + // Create shortcut path (foo.bar == foo.bar.bar) + keyWithShortPath = '' + path.join($nestedObjectDelimeter); + // Link it to original path + result[keyWithShortPath] = '@:' + keyWithPath; + } + result[keyWithPath] = val; + } + } + return result; + }; + flatObject.displayName = 'flatObject'; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#addInterpolation + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * Adds interpolation services to angular-translate, so it can manage them. + * + * @param {object} factory Interpolation service factory + */ + this.addInterpolation = function (factory) { + $interpolatorFactories.push(factory); + return this; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#useMessageFormatInterpolation + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * Tells angular-translate to use interpolation functionality of messageformat.js. + * This is useful when having high level pluralization and gender selection. + */ + this.useMessageFormatInterpolation = function () { + return this.useInterpolation('$translateMessageFormatInterpolation'); + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#useInterpolation + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * Tells angular-translate which interpolation style to use as default, application-wide. + * Simply pass a factory/service name. The interpolation service has to implement + * the correct interface. + * + * @param {string} factory Interpolation service name. + */ + this.useInterpolation = function (factory) { + $interpolationFactory = factory; + return this; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#useSanitizeStrategy + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * Simply sets a sanitation strategy type. + * + * @param {string} value Strategy type. + */ + this.useSanitizeValueStrategy = function (value) { + $translateSanitizationProvider.useStrategy(value); + return this; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#preferredLanguage + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * Tells the module which of the registered translation tables to use for translation + * at initial startup by passing a language key. Similar to `$translateProvider#use` + * only that it says which language to **prefer**. + * + * @param {string} langKey A language key. + */ + this.preferredLanguage = function(langKey) { + if (langKey) { + setupPreferredLanguage(langKey); + return this; + } + return $preferredLanguage; + }; + var setupPreferredLanguage = function (langKey) { + if (langKey) { + $preferredLanguage = langKey; + } + return $preferredLanguage; + }; + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#translationNotFoundIndicator + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * Sets an indicator which is used when a translation isn't found. E.g. when + * setting the indicator as 'X' and one tries to translate a translation id + * called `NOT_FOUND`, this will result in `X NOT_FOUND X`. + * + * Internally this methods sets a left indicator and a right indicator using + * `$translateProvider.translationNotFoundIndicatorLeft()` and + * `$translateProvider.translationNotFoundIndicatorRight()`. + * + * **Note**: These methods automatically add a whitespace between the indicators + * and the translation id. + * + * @param {string} indicator An indicator, could be any string. + */ + this.translationNotFoundIndicator = function (indicator) { + this.translationNotFoundIndicatorLeft(indicator); + this.translationNotFoundIndicatorRight(indicator); + return this; + }; + + /** + * ngdoc function + * @name pascalprecht.translate.$translateProvider#translationNotFoundIndicatorLeft + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * Sets an indicator which is used when a translation isn't found left to the + * translation id. + * + * @param {string} indicator An indicator. + */ + this.translationNotFoundIndicatorLeft = function (indicator) { + if (!indicator) { + return $notFoundIndicatorLeft; + } + $notFoundIndicatorLeft = indicator; + return this; + }; + + /** + * ngdoc function + * @name pascalprecht.translate.$translateProvider#translationNotFoundIndicatorLeft + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * Sets an indicator which is used when a translation isn't found right to the + * translation id. + * + * @param {string} indicator An indicator. + */ + this.translationNotFoundIndicatorRight = function (indicator) { + if (!indicator) { + return $notFoundIndicatorRight; + } + $notFoundIndicatorRight = indicator; + return this; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#fallbackLanguage + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * Tells the module which of the registered translation tables to use when missing translations + * at initial startup by passing a language key. Similar to `$translateProvider#use` + * only that it says which language to **fallback**. + * + * @param {string||array} langKey A language key. + * + */ + this.fallbackLanguage = function (langKey) { + fallbackStack(langKey); + return this; + }; + + var fallbackStack = function (langKey) { + if (langKey) { + if (angular.isString(langKey)) { + $fallbackWasString = true; + $fallbackLanguage = [ langKey ]; + } else if (angular.isArray(langKey)) { + $fallbackWasString = false; + $fallbackLanguage = langKey; + } + if (angular.isString($preferredLanguage) && indexOf($fallbackLanguage, $preferredLanguage) < 0) { + $fallbackLanguage.push($preferredLanguage); + } + + return this; + } else { + if ($fallbackWasString) { + return $fallbackLanguage[0]; + } else { + return $fallbackLanguage; + } + } + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#use + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * Set which translation table to use for translation by given language key. When + * trying to 'use' a language which isn't provided, it'll throw an error. + * + * You actually don't have to use this method since `$translateProvider#preferredLanguage` + * does the job too. + * + * @param {string} langKey A language key. + */ + this.use = function (langKey) { + if (langKey) { + if (!$translationTable[langKey] && (!$loaderFactory)) { + // only throw an error, when not loading translation data asynchronously + throw new Error('$translateProvider couldn\'t find translationTable for langKey: \'' + langKey + '\''); + } + $uses = langKey; + return this; + } + return $uses; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#resolveClientLocale + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * This returns the current browser/client's language key. The result is processed with the configured uniform tag resolver. + * + * @returns {string} the current client/browser language key + */ + this.resolveClientLocale = function () { + return getLocale(); + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#storageKey + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * Tells the module which key must represent the choosed language by a user in the storage. + * + * @param {string} key A key for the storage. + */ + var storageKey = function(key) { + if (!key) { + if ($storagePrefix) { + return $storagePrefix + $storageKey; + } + return $storageKey; + } + $storageKey = key; + return this; + }; + + this.storageKey = storageKey; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#useUrlLoader + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * Tells angular-translate to use `$translateUrlLoader` extension service as loader. + * + * @param {string} url Url + * @param {Object=} options Optional configuration object + */ + this.useUrlLoader = function (url, options) { + return this.useLoader('$translateUrlLoader', angular.extend({ url: url }, options)); + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#useStaticFilesLoader + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * Tells angular-translate to use `$translateStaticFilesLoader` extension service as loader. + * + * @param {Object=} options Optional configuration object + */ + this.useStaticFilesLoader = function (options) { + return this.useLoader('$translateStaticFilesLoader', options); + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#useLoader + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * Tells angular-translate to use any other service as loader. + * + * @param {string} loaderFactory Factory name to use + * @param {Object=} options Optional configuration object + */ + this.useLoader = function (loaderFactory, options) { + $loaderFactory = loaderFactory; + $loaderOptions = options || {}; + return this; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#useLocalStorage + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * Tells angular-translate to use `$translateLocalStorage` service as storage layer. + * + */ + this.useLocalStorage = function () { + return this.useStorage('$translateLocalStorage'); + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#useCookieStorage + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * Tells angular-translate to use `$translateCookieStorage` service as storage layer. + */ + this.useCookieStorage = function () { + return this.useStorage('$translateCookieStorage'); + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#useStorage + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * Tells angular-translate to use custom service as storage layer. + */ + this.useStorage = function (storageFactory) { + $storageFactory = storageFactory; + return this; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#storagePrefix + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * Sets prefix for storage key. + * + * @param {string} prefix Storage key prefix + */ + this.storagePrefix = function (prefix) { + if (!prefix) { + return prefix; + } + $storagePrefix = prefix; + return this; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#useMissingTranslationHandlerLog + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * Tells angular-translate to use built-in log handler when trying to translate + * a translation Id which doesn't exist. + * + * This is actually a shortcut method for `useMissingTranslationHandler()`. + * + */ + this.useMissingTranslationHandlerLog = function () { + return this.useMissingTranslationHandler('$translateMissingTranslationHandlerLog'); + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#useMissingTranslationHandler + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * Expects a factory name which later gets instantiated with `$injector`. + * This method can be used to tell angular-translate to use a custom + * missingTranslationHandler. Just build a factory which returns a function + * and expects a translation id as argument. + * + * Example: + *
      +   *  app.config(function ($translateProvider) {
      +   *    $translateProvider.useMissingTranslationHandler('customHandler');
      +   *  });
      +   *
      +   *  app.factory('customHandler', function (dep1, dep2) {
      +   *    return function (translationId) {
      +   *      // something with translationId and dep1 and dep2
      +   *    };
      +   *  });
      +   * 
      + * + * @param {string} factory Factory name + */ + this.useMissingTranslationHandler = function (factory) { + $missingTranslationHandlerFactory = factory; + return this; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#usePostCompiling + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * If post compiling is enabled, all translated values will be processed + * again with AngularJS' $compile. + * + * Example: + *
      +   *  app.config(function ($translateProvider) {
      +   *    $translateProvider.usePostCompiling(true);
      +   *  });
      +   * 
      + * + * @param {string} factory Factory name + */ + this.usePostCompiling = function (value) { + $postCompilingEnabled = !(!value); + return this; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#forceAsyncReload + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * If force async reload is enabled, async loader will always be called + * even if $translationTable already contains the language key, adding + * possible new entries to the $translationTable. + * + * Example: + *
      +   *  app.config(function ($translateProvider) {
      +   *    $translateProvider.forceAsyncReload(true);
      +   *  });
      +   * 
      + * + * @param {boolean} value - valid values are true or false + */ + this.forceAsyncReload = function (value) { + $forceAsyncReloadEnabled = !(!value); + return this; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#uniformLanguageTag + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * Tells angular-translate which language tag should be used as a result when determining + * the current browser language. + * + * This setting must be set before invoking {@link pascalprecht.translate.$translateProvider#methods_determinePreferredLanguage determinePreferredLanguage()}. + * + *
      +   * $translateProvider
      +   *   .uniformLanguageTag('bcp47')
      +   *   .determinePreferredLanguage()
      +   * 
      + * + * The resolver currently supports: + * * default + * (traditionally: hyphens will be converted into underscores, i.e. en-US => en_US) + * en-US => en_US + * en_US => en_US + * en-us => en_us + * * java + * like default, but the second part will be always in uppercase + * en-US => en_US + * en_US => en_US + * en-us => en_US + * * BCP 47 (RFC 4646 & 4647) + * en-US => en-US + * en_US => en-US + * en-us => en-US + * + * See also: + * * http://en.wikipedia.org/wiki/IETF_language_tag + * * http://www.w3.org/International/core/langtags/ + * * http://tools.ietf.org/html/bcp47 + * + * @param {string|object} options - options (or standard) + * @param {string} options.standard - valid values are 'default', 'bcp47', 'java' + */ + this.uniformLanguageTag = function (options) { + + if (!options) { + options = {}; + } else if (angular.isString(options)) { + options = { + standard: options + }; + } + + uniformLanguageTagResolver = options.standard; + + return this; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#determinePreferredLanguage + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * Tells angular-translate to try to determine on its own which language key + * to set as preferred language. When `fn` is given, angular-translate uses it + * to determine a language key, otherwise it uses the built-in `getLocale()` + * method. + * + * The `getLocale()` returns a language key in the format `[lang]_[country]` or + * `[lang]` depending on what the browser provides. + * + * Use this method at your own risk, since not all browsers return a valid + * locale (see {@link pascalprecht.translate.$translateProvider#methods_uniformLanguageTag uniformLanguageTag()}). + * + * @param {Function=} fn Function to determine a browser's locale + */ + this.determinePreferredLanguage = function (fn) { + + var locale = (fn && angular.isFunction(fn)) ? fn() : getLocale(); + + if (!$availableLanguageKeys.length) { + $preferredLanguage = locale; + } else { + $preferredLanguage = negotiateLocale(locale) || locale; + } + + return this; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#registerAvailableLanguageKeys + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * Registers a set of language keys the app will work with. Use this method in + * combination with + * {@link pascalprecht.translate.$translateProvider#determinePreferredLanguage determinePreferredLanguage}. + * When available languages keys are registered, angular-translate + * tries to find the best fitting language key depending on the browsers locale, + * considering your language key convention. + * + * @param {object} languageKeys Array of language keys the your app will use + * @param {object=} aliases Alias map. + */ + this.registerAvailableLanguageKeys = function (languageKeys, aliases) { + if (languageKeys) { + $availableLanguageKeys = languageKeys; + if (aliases) { + $languageKeyAliases = aliases; + } + return this; + } + return $availableLanguageKeys; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#useLoaderCache + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * Registers a cache for internal $http based loaders. + * {@link pascalprecht.translate.$translationCache $translationCache}. + * When false the cache will be disabled (default). When true or undefined + * the cache will be a default (see $cacheFactory). When an object it will + * be treat as a cache object itself: the usage is $http({cache: cache}) + * + * @param {object} cache boolean, string or cache-object + */ + this.useLoaderCache = function (cache) { + if (cache === false) { + // disable cache + loaderCache = undefined; + } else if (cache === true) { + // enable cache using AJS defaults + loaderCache = true; + } else if (typeof(cache) === 'undefined') { + // enable cache using default + loaderCache = '$translationCache'; + } else if (cache) { + // enable cache using given one (see $cacheFactory) + loaderCache = cache; + } + return this; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#directivePriority + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * Sets the default priority of the translate directive. The standard value is `0`. + * Calling this function without an argument will return the current value. + * + * @param {number} priority for the translate-directive + */ + this.directivePriority = function (priority) { + if (priority === undefined) { + // getter + return directivePriority; + } else { + // setter with chaining + directivePriority = priority; + return this; + } + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#statefulFilter + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * Since AngularJS 1.3, filters which are not stateless (depending at the scope) + * have to explicit define this behavior. + * Sets whether the translate filter should be stateful or stateless. The standard value is `true` + * meaning being stateful. + * Calling this function without an argument will return the current value. + * + * @param {boolean} state - defines the state of the filter + */ + this.statefulFilter = function (state) { + if (state === undefined) { + // getter + return statefulFilter; + } else { + // setter with chaining + statefulFilter = state; + return this; + } + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#postProcess + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * The post processor will be intercept right after the translation result. It can modify the result. + * + * @param {object} fn Function or service name (string) to be called after the translation value has been set / resolved. The function itself will enrich every value being processed and then continue the normal resolver process + */ + this.postProcess = function (fn) { + if (fn) { + postProcessFn = fn; + } else { + postProcessFn = undefined; + } + return this; + }; + + /** + * @ngdoc object + * @name pascalprecht.translate.$translate + * @requires $interpolate + * @requires $log + * @requires $rootScope + * @requires $q + * + * @description + * The `$translate` service is the actual core of angular-translate. It expects a translation id + * and optional interpolate parameters to translate contents. + * + *
      +   *  $translate('HEADLINE_TEXT').then(function (translation) {
      +   *    $scope.translatedText = translation;
      +   *  });
      +   * 
      + * + * @param {string|array} translationId A token which represents a translation id + * This can be optionally an array of translation ids which + * results that the function returns an object where each key + * is the translation id and the value the translation. + * @param {object=} interpolateParams An object hash for dynamic values + * @param {string} interpolationId The id of the interpolation to use + * @param {string} forceLanguage A language to be used instead of the current language + * @returns {object} promise + */ + this.$get = [ + '$log', + '$injector', + '$rootScope', + '$q', + function ($log, $injector, $rootScope, $q) { + + var Storage, + defaultInterpolator = $injector.get($interpolationFactory || '$translateDefaultInterpolation'), + pendingLoader = false, + interpolatorHashMap = {}, + langPromises = {}, + fallbackIndex, + startFallbackIteration; + + var $translate = function (translationId, interpolateParams, interpolationId, defaultTranslationText, forceLanguage) { + if (!$uses && $preferredLanguage) { + $uses = $preferredLanguage; + } + var uses = (forceLanguage && forceLanguage !== $uses) ? // we don't want to re-negotiate $uses + (negotiateLocale(forceLanguage) || forceLanguage) : $uses; + + // Check forceLanguage is present + if (forceLanguage) { + loadTranslationsIfMissing(forceLanguage); + } + + // Duck detection: If the first argument is an array, a bunch of translations was requested. + // The result is an object. + if (angular.isArray(translationId)) { + // Inspired by Q.allSettled by Kris Kowal + // https://github.com/kriskowal/q/blob/b0fa72980717dc202ffc3cbf03b936e10ebbb9d7/q.js#L1553-1563 + // This transforms all promises regardless resolved or rejected + var translateAll = function (translationIds) { + var results = {}; // storing the actual results + var promises = []; // promises to wait for + // Wraps the promise a) being always resolved and b) storing the link id->value + var translate = function (translationId) { + var deferred = $q.defer(); + var regardless = function (value) { + results[translationId] = value; + deferred.resolve([translationId, value]); + }; + // we don't care whether the promise was resolved or rejected; just store the values + $translate(translationId, interpolateParams, interpolationId, defaultTranslationText, forceLanguage).then(regardless, regardless); + return deferred.promise; + }; + for (var i = 0, c = translationIds.length; i < c; i++) { + promises.push(translate(translationIds[i])); + } + // wait for all (including storing to results) + return $q.all(promises).then(function () { + // return the results + return results; + }); + }; + return translateAll(translationId); + } + + var deferred = $q.defer(); + + // trim off any whitespace + if (translationId) { + translationId = trim.apply(translationId); + } + + var promiseToWaitFor = (function () { + var promise = $preferredLanguage ? + langPromises[$preferredLanguage] : + langPromises[uses]; + + fallbackIndex = 0; + + if ($storageFactory && !promise) { + // looks like there's no pending promise for $preferredLanguage or + // $uses. Maybe there's one pending for a language that comes from + // storage. + var langKey = Storage.get($storageKey); + promise = langPromises[langKey]; + + if ($fallbackLanguage && $fallbackLanguage.length) { + var index = indexOf($fallbackLanguage, langKey); + // maybe the language from storage is also defined as fallback language + // we increase the fallback language index to not search in that language + // as fallback, since it's probably the first used language + // in that case the index starts after the first element + fallbackIndex = (index === 0) ? 1 : 0; + + // but we can make sure to ALWAYS fallback to preferred language at least + if (indexOf($fallbackLanguage, $preferredLanguage) < 0) { + $fallbackLanguage.push($preferredLanguage); + } + } + } + return promise; + }()); + + if (!promiseToWaitFor) { + // no promise to wait for? okay. Then there's no loader registered + // nor is a one pending for language that comes from storage. + // We can just translate. + determineTranslation(translationId, interpolateParams, interpolationId, defaultTranslationText, uses).then(deferred.resolve, deferred.reject); + } else { + var promiseResolved = function () { + // $uses may have changed while waiting + if (!forceLanguage) { + uses = $uses; + } + determineTranslation(translationId, interpolateParams, interpolationId, defaultTranslationText, uses).then(deferred.resolve, deferred.reject); + }; + promiseResolved.displayName = 'promiseResolved'; + + promiseToWaitFor['finally'](promiseResolved); + } + return deferred.promise; + }; + + /** + * @name applyNotFoundIndicators + * @private + * + * @description + * Applies not fount indicators to given translation id, if needed. + * This function gets only executed, if a translation id doesn't exist, + * which is why a translation id is expected as argument. + * + * @param {string} translationId Translation id. + * @returns {string} Same as given translation id but applied with not found + * indicators. + */ + var applyNotFoundIndicators = function (translationId) { + // applying notFoundIndicators + if ($notFoundIndicatorLeft) { + translationId = [$notFoundIndicatorLeft, translationId].join(' '); + } + if ($notFoundIndicatorRight) { + translationId = [translationId, $notFoundIndicatorRight].join(' '); + } + return translationId; + }; + + /** + * @name useLanguage + * @private + * + * @description + * Makes actual use of a language by setting a given language key as used + * language and informs registered interpolators to also use the given + * key as locale. + * + * @param {key} Locale key. + */ + var useLanguage = function (key) { + $uses = key; + + // make sure to store new language key before triggering success event + if ($storageFactory) { + Storage.put($translate.storageKey(), $uses); + } + + $rootScope.$emit('$translateChangeSuccess', {language: key}); + + // inform default interpolator + defaultInterpolator.setLocale($uses); + + var eachInterpolator = function (interpolator, id) { + interpolatorHashMap[id].setLocale($uses); + }; + eachInterpolator.displayName = 'eachInterpolatorLocaleSetter'; + + // inform all others too! + angular.forEach(interpolatorHashMap, eachInterpolator); + $rootScope.$emit('$translateChangeEnd', {language: key}); + }; + + /** + * @name loadAsync + * @private + * + * @description + * Kicks of registered async loader using `$injector` and applies existing + * loader options. When resolved, it updates translation tables accordingly + * or rejects with given language key. + * + * @param {string} key Language key. + * @return {Promise} A promise. + */ + var loadAsync = function (key) { + if (!key) { + throw 'No language key specified for loading.'; + } + + var deferred = $q.defer(); + + $rootScope.$emit('$translateLoadingStart', {language: key}); + pendingLoader = true; + + var cache = loaderCache; + if (typeof(cache) === 'string') { + // getting on-demand instance of loader + cache = $injector.get(cache); + } + + var loaderOptions = angular.extend({}, $loaderOptions, { + key: key, + $http: angular.extend({}, { + cache: cache + }, $loaderOptions.$http) + }); + + var onLoaderSuccess = function (data) { + var translationTable = {}; + $rootScope.$emit('$translateLoadingSuccess', {language: key}); + + if (angular.isArray(data)) { + angular.forEach(data, function (table) { + angular.extend(translationTable, flatObject(table)); + }); + } else { + angular.extend(translationTable, flatObject(data)); + } + pendingLoader = false; + deferred.resolve({ + key: key, + table: translationTable + }); + $rootScope.$emit('$translateLoadingEnd', {language: key}); + }; + onLoaderSuccess.displayName = 'onLoaderSuccess'; + + var onLoaderError = function (key) { + $rootScope.$emit('$translateLoadingError', {language: key}); + deferred.reject(key); + $rootScope.$emit('$translateLoadingEnd', {language: key}); + }; + onLoaderError.displayName = 'onLoaderError'; + + $injector.get($loaderFactory)(loaderOptions) + .then(onLoaderSuccess, onLoaderError); + + return deferred.promise; + }; + + if ($storageFactory) { + Storage = $injector.get($storageFactory); + + if (!Storage.get || !Storage.put) { + throw new Error('Couldn\'t use storage \'' + $storageFactory + '\', missing get() or put() method!'); + } + } + + // if we have additional interpolations that were added via + // $translateProvider.addInterpolation(), we have to map'em + if ($interpolatorFactories.length) { + var eachInterpolationFactory = function (interpolatorFactory) { + var interpolator = $injector.get(interpolatorFactory); + // setting initial locale for each interpolation service + interpolator.setLocale($preferredLanguage || $uses); + // make'em recognizable through id + interpolatorHashMap[interpolator.getInterpolationIdentifier()] = interpolator; + }; + eachInterpolationFactory.displayName = 'interpolationFactoryAdder'; + + angular.forEach($interpolatorFactories, eachInterpolationFactory); + } + + /** + * @name getTranslationTable + * @private + * + * @description + * Returns a promise that resolves to the translation table + * or is rejected if an error occurred. + * + * @param langKey + * @returns {Q.promise} + */ + var getTranslationTable = function (langKey) { + var deferred = $q.defer(); + if (Object.prototype.hasOwnProperty.call($translationTable, langKey)) { + deferred.resolve($translationTable[langKey]); + } else if (langPromises[langKey]) { + var onResolve = function (data) { + translations(data.key, data.table); + deferred.resolve(data.table); + }; + onResolve.displayName = 'translationTableResolver'; + langPromises[langKey].then(onResolve, deferred.reject); + } else { + deferred.reject(); + } + return deferred.promise; + }; + + /** + * @name getFallbackTranslation + * @private + * + * @description + * Returns a promise that will resolve to the translation + * or be rejected if no translation was found for the language. + * This function is currently only used for fallback language translation. + * + * @param langKey The language to translate to. + * @param translationId + * @param interpolateParams + * @param Interpolator + * @returns {Q.promise} + */ + var getFallbackTranslation = function (langKey, translationId, interpolateParams, Interpolator) { + var deferred = $q.defer(); + + var onResolve = function (translationTable) { + if (Object.prototype.hasOwnProperty.call(translationTable, translationId)) { + Interpolator.setLocale(langKey); + var translation = translationTable[translationId]; + if (translation.substr(0, 2) === '@:') { + getFallbackTranslation(langKey, translation.substr(2), interpolateParams, Interpolator) + .then(deferred.resolve, deferred.reject); + } else { + var interpolatedValue = Interpolator.interpolate(translationTable[translationId], interpolateParams); + interpolatedValue = applyPostProcessing(translationId, translationTable[translationId], interpolatedValue, interpolateParams, langKey); + + deferred.resolve(interpolatedValue); + + } + Interpolator.setLocale($uses); + } else { + deferred.reject(); + } + }; + onResolve.displayName = 'fallbackTranslationResolver'; + + getTranslationTable(langKey).then(onResolve, deferred.reject); + + return deferred.promise; + }; + + /** + * @name getFallbackTranslationInstant + * @private + * + * @description + * Returns a translation + * This function is currently only used for fallback language translation. + * + * @param langKey The language to translate to. + * @param translationId + * @param interpolateParams + * @param Interpolator + * @returns {string} translation + */ + var getFallbackTranslationInstant = function (langKey, translationId, interpolateParams, Interpolator) { + var result, translationTable = $translationTable[langKey]; + + if (translationTable && Object.prototype.hasOwnProperty.call(translationTable, translationId)) { + Interpolator.setLocale(langKey); + result = Interpolator.interpolate(translationTable[translationId], interpolateParams); + if (result.substr(0, 2) === '@:') { + return getFallbackTranslationInstant(langKey, result.substr(2), interpolateParams, Interpolator); + } + Interpolator.setLocale($uses); + } + + return result; + }; + + + /** + * @name translateByHandler + * @private + * + * Translate by missing translation handler. + * + * @param translationId + * @param interpolateParams + * @param defaultTranslationText + * @returns translation created by $missingTranslationHandler or translationId is $missingTranslationHandler is + * absent + */ + var translateByHandler = function (translationId, interpolateParams, defaultTranslationText) { + // If we have a handler factory - we might also call it here to determine if it provides + // a default text for a translationid that can't be found anywhere in our tables + if ($missingTranslationHandlerFactory) { + var resultString = $injector.get($missingTranslationHandlerFactory)(translationId, $uses, interpolateParams, defaultTranslationText); + if (resultString !== undefined) { + return resultString; + } else { + return translationId; + } + } else { + return translationId; + } + }; + + /** + * @name resolveForFallbackLanguage + * @private + * + * Recursive helper function for fallbackTranslation that will sequentially look + * for a translation in the fallbackLanguages starting with fallbackLanguageIndex. + * + * @param fallbackLanguageIndex + * @param translationId + * @param interpolateParams + * @param Interpolator + * @returns {Q.promise} Promise that will resolve to the translation. + */ + var resolveForFallbackLanguage = function (fallbackLanguageIndex, translationId, interpolateParams, Interpolator, defaultTranslationText) { + var deferred = $q.defer(); + + if (fallbackLanguageIndex < $fallbackLanguage.length) { + var langKey = $fallbackLanguage[fallbackLanguageIndex]; + getFallbackTranslation(langKey, translationId, interpolateParams, Interpolator).then( + function (data) { + deferred.resolve(data); + }, + function () { + // Look in the next fallback language for a translation. + // It delays the resolving by passing another promise to resolve. + return resolveForFallbackLanguage(fallbackLanguageIndex + 1, translationId, interpolateParams, Interpolator, defaultTranslationText).then(deferred.resolve, deferred.reject); + } + ); + } else { + // No translation found in any fallback language + // if a default translation text is set in the directive, then return this as a result + if (defaultTranslationText) { + deferred.resolve(defaultTranslationText); + } else { + // if no default translation is set and an error handler is defined, send it to the handler + // and then return the result + if ($missingTranslationHandlerFactory) { + deferred.resolve(translateByHandler(translationId, interpolateParams)); + } else { + deferred.reject(translateByHandler(translationId, interpolateParams)); + } + + } + } + return deferred.promise; + }; + + /** + * @name resolveForFallbackLanguageInstant + * @private + * + * Recursive helper function for fallbackTranslation that will sequentially look + * for a translation in the fallbackLanguages starting with fallbackLanguageIndex. + * + * @param fallbackLanguageIndex + * @param translationId + * @param interpolateParams + * @param Interpolator + * @returns {string} translation + */ + var resolveForFallbackLanguageInstant = function (fallbackLanguageIndex, translationId, interpolateParams, Interpolator) { + var result; + + if (fallbackLanguageIndex < $fallbackLanguage.length) { + var langKey = $fallbackLanguage[fallbackLanguageIndex]; + result = getFallbackTranslationInstant(langKey, translationId, interpolateParams, Interpolator); + if (!result) { + result = resolveForFallbackLanguageInstant(fallbackLanguageIndex + 1, translationId, interpolateParams, Interpolator); + } + } + return result; + }; + + /** + * Translates with the usage of the fallback languages. + * + * @param translationId + * @param interpolateParams + * @param Interpolator + * @returns {Q.promise} Promise, that resolves to the translation. + */ + var fallbackTranslation = function (translationId, interpolateParams, Interpolator, defaultTranslationText) { + // Start with the fallbackLanguage with index 0 + return resolveForFallbackLanguage((startFallbackIteration>0 ? startFallbackIteration : fallbackIndex), translationId, interpolateParams, Interpolator, defaultTranslationText); + }; + + /** + * Translates with the usage of the fallback languages. + * + * @param translationId + * @param interpolateParams + * @param Interpolator + * @returns {String} translation + */ + var fallbackTranslationInstant = function (translationId, interpolateParams, Interpolator) { + // Start with the fallbackLanguage with index 0 + return resolveForFallbackLanguageInstant((startFallbackIteration>0 ? startFallbackIteration : fallbackIndex), translationId, interpolateParams, Interpolator); + }; + + var determineTranslation = function (translationId, interpolateParams, interpolationId, defaultTranslationText, uses) { + + var deferred = $q.defer(); + + var table = uses ? $translationTable[uses] : $translationTable, + Interpolator = (interpolationId) ? interpolatorHashMap[interpolationId] : defaultInterpolator; + + // if the translation id exists, we can just interpolate it + if (table && Object.prototype.hasOwnProperty.call(table, translationId)) { + var translation = table[translationId]; + + // If using link, rerun $translate with linked translationId and return it + if (translation.substr(0, 2) === '@:') { + + $translate(translation.substr(2), interpolateParams, interpolationId, defaultTranslationText, uses) + .then(deferred.resolve, deferred.reject); + } else { + // + var resolvedTranslation = Interpolator.interpolate(translation, interpolateParams); + resolvedTranslation = applyPostProcessing(translationId, translation, resolvedTranslation, interpolateParams, uses); + deferred.resolve(resolvedTranslation); + } + } else { + var missingTranslationHandlerTranslation; + // for logging purposes only (as in $translateMissingTranslationHandlerLog), value is not returned to promise + if ($missingTranslationHandlerFactory && !pendingLoader) { + missingTranslationHandlerTranslation = translateByHandler(translationId, interpolateParams, defaultTranslationText); + } + + // since we couldn't translate the inital requested translation id, + // we try it now with one or more fallback languages, if fallback language(s) is + // configured. + if (uses && $fallbackLanguage && $fallbackLanguage.length) { + fallbackTranslation(translationId, interpolateParams, Interpolator, defaultTranslationText) + .then(function (translation) { + deferred.resolve(translation); + }, function (_translationId) { + deferred.reject(applyNotFoundIndicators(_translationId)); + }); + } else if ($missingTranslationHandlerFactory && !pendingLoader && missingTranslationHandlerTranslation) { + // looks like the requested translation id doesn't exists. + // Now, if there is a registered handler for missing translations and no + // asyncLoader is pending, we execute the handler + if (defaultTranslationText) { + deferred.resolve(defaultTranslationText); + } else { + deferred.resolve(missingTranslationHandlerTranslation); + } + } else { + if (defaultTranslationText) { + deferred.resolve(defaultTranslationText); + } else { + deferred.reject(applyNotFoundIndicators(translationId)); + } + } + } + return deferred.promise; + }; + + var determineTranslationInstant = function (translationId, interpolateParams, interpolationId, uses) { + + var result, table = uses ? $translationTable[uses] : $translationTable, + Interpolator = defaultInterpolator; + + // if the interpolation id exists use custom interpolator + if (interpolatorHashMap && Object.prototype.hasOwnProperty.call(interpolatorHashMap, interpolationId)) { + Interpolator = interpolatorHashMap[interpolationId]; + } + + // if the translation id exists, we can just interpolate it + if (table && Object.prototype.hasOwnProperty.call(table, translationId)) { + var translation = table[translationId]; + + // If using link, rerun $translate with linked translationId and return it + if (translation.substr(0, 2) === '@:') { + result = determineTranslationInstant(translation.substr(2), interpolateParams, interpolationId, uses); + } else { + result = Interpolator.interpolate(translation, interpolateParams); + } + } else { + var missingTranslationHandlerTranslation; + // for logging purposes only (as in $translateMissingTranslationHandlerLog), value is not returned to promise + if ($missingTranslationHandlerFactory && !pendingLoader) { + missingTranslationHandlerTranslation = translateByHandler(translationId, interpolateParams); + } + + // since we couldn't translate the inital requested translation id, + // we try it now with one or more fallback languages, if fallback language(s) is + // configured. + if (uses && $fallbackLanguage && $fallbackLanguage.length) { + fallbackIndex = 0; + result = fallbackTranslationInstant(translationId, interpolateParams, Interpolator); + } else if ($missingTranslationHandlerFactory && !pendingLoader && missingTranslationHandlerTranslation) { + // looks like the requested translation id doesn't exists. + // Now, if there is a registered handler for missing translations and no + // asyncLoader is pending, we execute the handler + result = missingTranslationHandlerTranslation; + } else { + result = applyNotFoundIndicators(translationId); + } + } + + return result; + }; + + var clearNextLangAndPromise = function(key) { + if ($nextLang === key) { + $nextLang = undefined; + } + langPromises[key] = undefined; + }; + + var applyPostProcessing = function (translationId, translation, resolvedTranslation, interpolateParams, uses) { + var fn = postProcessFn; + + if (fn) { + + if (typeof(fn) === 'string') { + // getting on-demand instance + fn = $injector.get(fn); + } + if (fn) { + return fn(translationId, translation, resolvedTranslation, interpolateParams, uses); + } + } + + return resolvedTranslation; + }; + + var loadTranslationsIfMissing = function (key) { + if (!$translationTable[key] && $loaderFactory && !langPromises[key]) { + langPromises[key] = loadAsync(key).then(function (translation) { + translations(translation.key, translation.table); + }); + } + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translate#preferredLanguage + * @methodOf pascalprecht.translate.$translate + * + * @description + * Returns the language key for the preferred language. + * + * @param {string} langKey language String or Array to be used as preferredLanguage (changing at runtime) + * + * @return {string} preferred language key + */ + $translate.preferredLanguage = function (langKey) { + if(langKey) { + setupPreferredLanguage(langKey); + } + return $preferredLanguage; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translate#cloakClassName + * @methodOf pascalprecht.translate.$translate + * + * @description + * Returns the configured class name for `translate-cloak` directive. + * + * @return {string} cloakClassName + */ + $translate.cloakClassName = function () { + return $cloakClassName; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translate#nestedObjectDelimeter + * @methodOf pascalprecht.translate.$translate + * + * @description + * Returns the configured delimiter for nested namespaces. + * + * @return {string} nestedObjectDelimeter + */ + $translate.nestedObjectDelimeter = function () { + return $nestedObjectDelimeter; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translate#fallbackLanguage + * @methodOf pascalprecht.translate.$translate + * + * @description + * Returns the language key for the fallback languages or sets a new fallback stack. + * + * @param {string=} langKey language String or Array of fallback languages to be used (to change stack at runtime) + * + * @return {string||array} fallback language key + */ + $translate.fallbackLanguage = function (langKey) { + if (langKey !== undefined && langKey !== null) { + fallbackStack(langKey); + + // as we might have an async loader initiated and a new translation language might have been defined + // we need to add the promise to the stack also. So - iterate. + if ($loaderFactory) { + if ($fallbackLanguage && $fallbackLanguage.length) { + for (var i = 0, len = $fallbackLanguage.length; i < len; i++) { + if (!langPromises[$fallbackLanguage[i]]) { + langPromises[$fallbackLanguage[i]] = loadAsync($fallbackLanguage[i]); + } + } + } + } + $translate.use($translate.use()); + } + if ($fallbackWasString) { + return $fallbackLanguage[0]; + } else { + return $fallbackLanguage; + } + + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translate#useFallbackLanguage + * @methodOf pascalprecht.translate.$translate + * + * @description + * Sets the first key of the fallback language stack to be used for translation. + * Therefore all languages in the fallback array BEFORE this key will be skipped! + * + * @param {string=} langKey Contains the langKey the iteration shall start with. Set to false if you want to + * get back to the whole stack + */ + $translate.useFallbackLanguage = function (langKey) { + if (langKey !== undefined && langKey !== null) { + if (!langKey) { + startFallbackIteration = 0; + } else { + var langKeyPosition = indexOf($fallbackLanguage, langKey); + if (langKeyPosition > -1) { + startFallbackIteration = langKeyPosition; + } + } + + } + + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translate#proposedLanguage + * @methodOf pascalprecht.translate.$translate + * + * @description + * Returns the language key of language that is currently loaded asynchronously. + * + * @return {string} language key + */ + $translate.proposedLanguage = function () { + return $nextLang; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translate#storage + * @methodOf pascalprecht.translate.$translate + * + * @description + * Returns registered storage. + * + * @return {object} Storage + */ + $translate.storage = function () { + return Storage; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translate#negotiateLocale + * @methodOf pascalprecht.translate.$translate + * + * @description + * Returns a language key based on available languages and language aliases. If a + * language key cannot be resolved, returns undefined. + * + * If no or a falsy key is given, returns undefined. + * + * @param {string} [key] Language key + * @return {string|undefined} Language key or undefined if no language key is found. + */ + $translate.negotiateLocale = negotiateLocale; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translate#use + * @methodOf pascalprecht.translate.$translate + * + * @description + * Tells angular-translate which language to use by given language key. This method is + * used to change language at runtime. It also takes care of storing the language + * key in a configured store to let your app remember the choosed language. + * + * When trying to 'use' a language which isn't available it tries to load it + * asynchronously with registered loaders. + * + * Returns promise object with loaded language file data or string of the currently used language. + * + * If no or a falsy key is given it returns the currently used language key. + * The returned string will be ```undefined``` if setting up $translate hasn't finished. + * @example + * $translate.use("en_US").then(function(data){ + * $scope.text = $translate("HELLO"); + * }); + * + * @param {string} [key] Language key + * @return {object|string} Promise with loaded language data or the language key if a falsy param was given. + */ + $translate.use = function (key) { + if (!key) { + return $uses; + } + + var deferred = $q.defer(); + + $rootScope.$emit('$translateChangeStart', {language: key}); + + // Try to get the aliased language key + var aliasedKey = negotiateLocale(key); + // Ensure only registered language keys will be loaded + if ($availableLanguageKeys.length > 0 && !aliasedKey) { + return $q.reject(key); + } + + if (aliasedKey) { + key = aliasedKey; + } + + // if there isn't a translation table for the language we've requested, + // we load it asynchronously + $nextLang = key; + if (($forceAsyncReloadEnabled || !$translationTable[key]) && $loaderFactory && !langPromises[key]) { + langPromises[key] = loadAsync(key).then(function (translation) { + translations(translation.key, translation.table); + deferred.resolve(translation.key); + if ($nextLang === key) { + useLanguage(translation.key); + } + return translation; + }, function (key) { + $rootScope.$emit('$translateChangeError', {language: key}); + deferred.reject(key); + $rootScope.$emit('$translateChangeEnd', {language: key}); + return $q.reject(key); + }); + langPromises[key]['finally'](function () { + clearNextLangAndPromise(key); + }); + } else if (langPromises[key]) { + // we are already loading this asynchronously + // resolve our new deferred when the old langPromise is resolved + langPromises[key].then(function (translation) { + if ($nextLang === translation.key) { + useLanguage(translation.key); + } + deferred.resolve(translation.key); + return translation; + }, function (key) { + // find first available fallback language if that request has failed + if (!$uses && $fallbackLanguage && $fallbackLanguage.length > 0) { + return $translate.use($fallbackLanguage[0]).then(deferred.resolve, deferred.reject); + } else { + return deferred.reject(key); + } + }); + } else { + deferred.resolve(key); + useLanguage(key); + } + + return deferred.promise; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translate#resolveClientLocale + * @methodOf pascalprecht.translate.$translate + * + * @description + * This returns the current browser/client's language key. The result is processed with the configured uniform tag resolver. + * + * @returns {string} the current client/browser language key + */ + $translate.resolveClientLocale = function () { + return getLocale(); + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translate#storageKey + * @methodOf pascalprecht.translate.$translate + * + * @description + * Returns the key for the storage. + * + * @return {string} storage key + */ + $translate.storageKey = function () { + return storageKey(); + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translate#isPostCompilingEnabled + * @methodOf pascalprecht.translate.$translate + * + * @description + * Returns whether post compiling is enabled or not + * + * @return {bool} storage key + */ + $translate.isPostCompilingEnabled = function () { + return $postCompilingEnabled; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translate#isForceAsyncReloadEnabled + * @methodOf pascalprecht.translate.$translate + * + * @description + * Returns whether force async reload is enabled or not + * + * @return {boolean} forceAsyncReload value + */ + $translate.isForceAsyncReloadEnabled = function () { + return $forceAsyncReloadEnabled; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translate#refresh + * @methodOf pascalprecht.translate.$translate + * + * @description + * Refreshes a translation table pointed by the given langKey. If langKey is not specified, + * the module will drop all existent translation tables and load new version of those which + * are currently in use. + * + * Refresh means that the module will drop target translation table and try to load it again. + * + * In case there are no loaders registered the refresh() method will throw an Error. + * + * If the module is able to refresh translation tables refresh() method will broadcast + * $translateRefreshStart and $translateRefreshEnd events. + * + * @example + * // this will drop all currently existent translation tables and reload those which are + * // currently in use + * $translate.refresh(); + * // this will refresh a translation table for the en_US language + * $translate.refresh('en_US'); + * + * @param {string} langKey A language key of the table, which has to be refreshed + * + * @return {promise} Promise, which will be resolved in case a translation tables refreshing + * process is finished successfully, and reject if not. + */ + $translate.refresh = function (langKey) { + if (!$loaderFactory) { + throw new Error('Couldn\'t refresh translation table, no loader registered!'); + } + + var deferred = $q.defer(); + + function resolve() { + deferred.resolve(); + $rootScope.$emit('$translateRefreshEnd', {language: langKey}); + } + + function reject() { + deferred.reject(); + $rootScope.$emit('$translateRefreshEnd', {language: langKey}); + } + + $rootScope.$emit('$translateRefreshStart', {language: langKey}); + + if (!langKey) { + // if there's no language key specified we refresh ALL THE THINGS! + var tables = [], loadingKeys = {}; + + // reload registered fallback languages + if ($fallbackLanguage && $fallbackLanguage.length) { + for (var i = 0, len = $fallbackLanguage.length; i < len; i++) { + tables.push(loadAsync($fallbackLanguage[i])); + loadingKeys[$fallbackLanguage[i]] = true; + } + } + + // reload currently used language + if ($uses && !loadingKeys[$uses]) { + tables.push(loadAsync($uses)); + } + + var allTranslationsLoaded = function (tableData) { + $translationTable = {}; + angular.forEach(tableData, function (data) { + translations(data.key, data.table); + }); + if ($uses) { + useLanguage($uses); + } + resolve(); + }; + allTranslationsLoaded.displayName = 'refreshPostProcessor'; + + $q.all(tables).then(allTranslationsLoaded, reject); + + } else if ($translationTable[langKey]) { + + var oneTranslationsLoaded = function (data) { + translations(data.key, data.table); + if (langKey === $uses) { + useLanguage($uses); + } + resolve(); + }; + oneTranslationsLoaded.displayName = 'refreshPostProcessor'; + + loadAsync(langKey).then(oneTranslationsLoaded, reject); + + } else { + reject(); + } + return deferred.promise; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translate#instant + * @methodOf pascalprecht.translate.$translate + * + * @description + * Returns a translation instantly from the internal state of loaded translation. All rules + * regarding the current language, the preferred language of even fallback languages will be + * used except any promise handling. If a language was not found, an asynchronous loading + * will be invoked in the background. + * + * @param {string|array} translationId A token which represents a translation id + * This can be optionally an array of translation ids which + * results that the function's promise returns an object where + * each key is the translation id and the value the translation. + * @param {object} interpolateParams Params + * @param {string} interpolationId The id of the interpolation to use + * @param {string} forceLanguage A language to be used instead of the current language + * + * @return {string|object} translation + */ + $translate.instant = function (translationId, interpolateParams, interpolationId, forceLanguage) { + + // we don't want to re-negotiate $uses + var uses = (forceLanguage && forceLanguage !== $uses) ? // we don't want to re-negotiate $uses + (negotiateLocale(forceLanguage) || forceLanguage) : $uses; + + // Detect undefined and null values to shorten the execution and prevent exceptions + if (translationId === null || angular.isUndefined(translationId)) { + return translationId; + } + + // Check forceLanguage is present + if (forceLanguage) { + loadTranslationsIfMissing(forceLanguage); + } + + // Duck detection: If the first argument is an array, a bunch of translations was requested. + // The result is an object. + if (angular.isArray(translationId)) { + var results = {}; + for (var i = 0, c = translationId.length; i < c; i++) { + results[translationId[i]] = $translate.instant(translationId[i], interpolateParams, interpolationId, forceLanguage); + } + return results; + } + + // We discarded unacceptable values. So we just need to verify if translationId is empty String + if (angular.isString(translationId) && translationId.length < 1) { + return translationId; + } + + // trim off any whitespace + if (translationId) { + translationId = trim.apply(translationId); + } + + var result, possibleLangKeys = []; + if ($preferredLanguage) { + possibleLangKeys.push($preferredLanguage); + } + if (uses) { + possibleLangKeys.push(uses); + } + if ($fallbackLanguage && $fallbackLanguage.length) { + possibleLangKeys = possibleLangKeys.concat($fallbackLanguage); + } + for (var j = 0, d = possibleLangKeys.length; j < d; j++) { + var possibleLangKey = possibleLangKeys[j]; + if ($translationTable[possibleLangKey]) { + if (typeof $translationTable[possibleLangKey][translationId] !== 'undefined') { + result = determineTranslationInstant(translationId, interpolateParams, interpolationId, uses); + } + } + if (typeof result !== 'undefined') { + break; + } + } + + if (!result && result !== '') { + if ($notFoundIndicatorLeft || $notFoundIndicatorRight) { + result = applyNotFoundIndicators(translationId); + } else { + // Return translation of default interpolator if not found anything. + result = defaultInterpolator.interpolate(translationId, interpolateParams); + if ($missingTranslationHandlerFactory && !pendingLoader) { + result = translateByHandler(translationId, interpolateParams); + } + } + } + + return result; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translate#versionInfo + * @methodOf pascalprecht.translate.$translate + * + * @description + * Returns the current version information for the angular-translate library + * + * @return {string} angular-translate version + */ + $translate.versionInfo = function () { + return version; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translate#loaderCache + * @methodOf pascalprecht.translate.$translate + * + * @description + * Returns the defined loaderCache. + * + * @return {boolean|string|object} current value of loaderCache + */ + $translate.loaderCache = function () { + return loaderCache; + }; + + // internal purpose only + $translate.directivePriority = function () { + return directivePriority; + }; + + // internal purpose only + $translate.statefulFilter = function () { + return statefulFilter; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translate#isReady + * @methodOf pascalprecht.translate.$translate + * + * @description + * Returns whether the service is "ready" to translate (i.e. loading 1st language). + * + * See also {@link pascalprecht.translate.$translate#methods_onReady onReady()}. + * + * @return {boolean} current value of ready + */ + $translate.isReady = function () { + return $isReady; + }; + + var $onReadyDeferred = $q.defer(); + $onReadyDeferred.promise.then(function () { + $isReady = true; + }); + + /** + * @ngdoc function + * @name pascalprecht.translate.$translate#onReady + * @methodOf pascalprecht.translate.$translate + * + * @description + * Returns whether the service is "ready" to translate (i.e. loading 1st language). + * + * See also {@link pascalprecht.translate.$translate#methods_isReady isReady()}. + * + * @param {Function=} fn Function to invoke when service is ready + * @return {object} Promise resolved when service is ready + */ + $translate.onReady = function (fn) { + var deferred = $q.defer(); + if (angular.isFunction(fn)) { + deferred.promise.then(fn); + } + if ($isReady) { + deferred.resolve(); + } else { + $onReadyDeferred.promise.then(deferred.resolve); + } + return deferred.promise; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translate#getAvailableLanguageKeys + * @methodOf pascalprecht.translate.$translate + * + * @description + * This function simply returns the registered language keys being defined before in the config phase + * With this, an application can use the array to provide a language selection dropdown or similar + * without any additional effort + * + * @returns {object} returns the list of possibly registered language keys and mapping or null if not defined + */ + $translate.getAvailableLanguageKeys = function () { + if ($availableLanguageKeys.length > 0) { + return $availableLanguageKeys; + } + return null; + }; + + // Whenever $translateReady is being fired, this will ensure the state of $isReady + var globalOnReadyListener = $rootScope.$on('$translateReady', function () { + $onReadyDeferred.resolve(); + globalOnReadyListener(); // one time only + globalOnReadyListener = null; + }); + var globalOnChangeListener = $rootScope.$on('$translateChangeEnd', function () { + $onReadyDeferred.resolve(); + globalOnChangeListener(); // one time only + globalOnChangeListener = null; + }); + + if ($loaderFactory) { + + // If at least one async loader is defined and there are no + // (default) translations available we should try to load them. + if (angular.equals($translationTable, {})) { + if ($translate.use()) { + $translate.use($translate.use()); + } + } + + // Also, if there are any fallback language registered, we start + // loading them asynchronously as soon as we can. + if ($fallbackLanguage && $fallbackLanguage.length) { + var processAsyncResult = function (translation) { + translations(translation.key, translation.table); + $rootScope.$emit('$translateChangeEnd', { language: translation.key }); + return translation; + }; + for (var i = 0, len = $fallbackLanguage.length; i < len; i++) { + var fallbackLanguageId = $fallbackLanguage[i]; + if ($forceAsyncReloadEnabled || !$translationTable[fallbackLanguageId]) { + langPromises[fallbackLanguageId] = loadAsync(fallbackLanguageId).then(processAsyncResult); + } + } + } + } else { + $rootScope.$emit('$translateReady', { language: $translate.use() }); + } + + return $translate; + } + ]; +} + +$translate.displayName = 'displayName'; + +/** + * @ngdoc object + * @name pascalprecht.translate.$translateDefaultInterpolation + * @requires $interpolate + * + * @description + * Uses angular's `$interpolate` services to interpolate strings against some values. + * + * Be aware to configure a proper sanitization strategy. + * + * See also: + * * {@link pascalprecht.translate.$translateSanitization} + * + * @return {object} $translateDefaultInterpolation Interpolator service + */ +angular.module('pascalprecht.translate').factory('$translateDefaultInterpolation', $translateDefaultInterpolation); + +function $translateDefaultInterpolation ($interpolate, $translateSanitization) { + + 'use strict'; + + var $translateInterpolator = {}, + $locale, + $identifier = 'default'; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateDefaultInterpolation#setLocale + * @methodOf pascalprecht.translate.$translateDefaultInterpolation + * + * @description + * Sets current locale (this is currently not use in this interpolation). + * + * @param {string} locale Language key or locale. + */ + $translateInterpolator.setLocale = function (locale) { + $locale = locale; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateDefaultInterpolation#getInterpolationIdentifier + * @methodOf pascalprecht.translate.$translateDefaultInterpolation + * + * @description + * Returns an identifier for this interpolation service. + * + * @returns {string} $identifier + */ + $translateInterpolator.getInterpolationIdentifier = function () { + return $identifier; + }; + + /** + * @deprecated will be removed in 3.0 + * @see {@link pascalprecht.translate.$translateSanitization} + */ + $translateInterpolator.useSanitizeValueStrategy = function (value) { + $translateSanitization.useStrategy(value); + return this; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateDefaultInterpolation#interpolate + * @methodOf pascalprecht.translate.$translateDefaultInterpolation + * + * @description + * Interpolates given string agains given interpolate params using angulars + * `$interpolate` service. + * + * @returns {string} interpolated string. + */ + $translateInterpolator.interpolate = function (string, interpolationParams) { + interpolationParams = interpolationParams || {}; + interpolationParams = $translateSanitization.sanitize(interpolationParams, 'params'); + + var interpolatedText = $interpolate(string)(interpolationParams); + interpolatedText = $translateSanitization.sanitize(interpolatedText, 'text'); + + return interpolatedText; + }; + + return $translateInterpolator; +} + +$translateDefaultInterpolation.displayName = '$translateDefaultInterpolation'; + +angular.module('pascalprecht.translate').constant('$STORAGE_KEY', 'NG_TRANSLATE_LANG_KEY'); + +angular.module('pascalprecht.translate') +/** + * @ngdoc directive + * @name pascalprecht.translate.directive:translate + * @requires $compile + * @requires $filter + * @requires $interpolate + * @restrict AE + * + * @description + * Translates given translation id either through attribute or DOM content. + * Internally it uses `translate` filter to translate translation id. It possible to + * pass an optional `translate-values` object literal as string into translation id. + * + * @param {string=} translate Translation id which could be either string or interpolated string. + * @param {string=} translate-values Values to pass into translation id. Can be passed as object literal string or interpolated object. + * @param {string=} translate-attr-ATTR translate Translation id and put it into ATTR attribute. + * @param {string=} translate-default will be used unless translation was successful + * @param {boolean=} translate-compile (default true if present) defines locally activation of {@link pascalprecht.translate.$translateProvider#methods_usePostCompiling} + * @param {boolean=} translate-keep-content (default true if present) defines that in case a KEY could not be translated, that the existing content is left in the innerHTML} + * + * @example + + +
      + +
      
      +        
      TRANSLATION_ID
      +
      
      +        
      
      +        
      {{translationId}}
      +
      
      +        
      WITH_VALUES
      +
      
      +        
      WITH_VALUES
      +
      
      +
      +      
      +
      + + 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.text()).toBe('Hello there!'); + + element = $compile('

      ')($rootScope); + $rootScope.$digest(); + expect(element.text()).toBe('Hello there!'); + + element = $compile('

      TRANSLATION_ID

      ')($rootScope); + $rootScope.$digest(); + expect(element.text()).toBe('Hello there!'); + + element = $compile('

      {{translationId}}

      ')($rootScope); + $rootScope.$digest(); + expect(element.text()).toBe('Hello there!'); + + element = $compile('

      ')($rootScope); + $rootScope.$digest(); + expect(element.attr('title')).toBe('Hello there!'); + }); + }); +
      +
      + */ +.directive('translate', translateDirective); +function translateDirective($translate, $q, $interpolate, $compile, $parse, $rootScope) { + + 'use strict'; + + /** + * @name trim + * @private + * + * @description + * trim polyfill + * + * @returns {string} The string stripped of whitespace from both ends + */ + var trim = function() { + return this.toString().replace(/^\s+|\s+$/g, ''); + }; + + return { + restrict: 'AE', + scope: true, + priority: $translate.directivePriority(), + compile: function (tElement, tAttr) { + + var translateValuesExist = (tAttr.translateValues) ? + tAttr.translateValues : undefined; + + var translateInterpolation = (tAttr.translateInterpolation) ? + tAttr.translateInterpolation : undefined; + + var translateValueExist = tElement[0].outerHTML.match(/translate-value-+/i); + + var interpolateRegExp = '^(.*)(' + $interpolate.startSymbol() + '.*' + $interpolate.endSymbol() + ')(.*)', + watcherRegExp = '^(.*)' + $interpolate.startSymbol() + '(.*)' + $interpolate.endSymbol() + '(.*)'; + + return function linkFn(scope, iElement, iAttr) { + + scope.interpolateParams = {}; + scope.preText = ''; + scope.postText = ''; + scope.translateNamespace = getTranslateNamespace(scope); + var translationIds = {}; + + var initInterpolationParams = function (interpolateParams, iAttr, tAttr) { + // initial setup + if (iAttr.translateValues) { + angular.extend(interpolateParams, $parse(iAttr.translateValues)(scope.$parent)); + } + // initially fetch all attributes if existing and fill the params + if (translateValueExist) { + for (var attr in tAttr) { + if (Object.prototype.hasOwnProperty.call(iAttr, attr) && attr.substr(0, 14) === 'translateValue' && attr !== 'translateValues') { + var attributeName = angular.lowercase(attr.substr(14, 1)) + attr.substr(15); + interpolateParams[attributeName] = tAttr[attr]; + } + } + } + }; + + // Ensures any change of the attribute "translate" containing the id will + // be re-stored to the scope's "translationId". + // If the attribute has no content, the element's text value (white spaces trimmed off) will be used. + var observeElementTranslation = function (translationId) { + + // Remove any old watcher + if (angular.isFunction(observeElementTranslation._unwatchOld)) { + observeElementTranslation._unwatchOld(); + observeElementTranslation._unwatchOld = undefined; + } + + if (angular.equals(translationId , '') || !angular.isDefined(translationId)) { + var iElementText = trim.apply(iElement.text()); + + // Resolve translation id by inner html if required + var interpolateMatches = iElementText.match(interpolateRegExp); + // Interpolate translation id if required + if (angular.isArray(interpolateMatches)) { + scope.preText = interpolateMatches[1]; + scope.postText = interpolateMatches[3]; + translationIds.translate = $interpolate(interpolateMatches[2])(scope.$parent); + var watcherMatches = iElementText.match(watcherRegExp); + if (angular.isArray(watcherMatches) && watcherMatches[2] && watcherMatches[2].length) { + observeElementTranslation._unwatchOld = scope.$watch(watcherMatches[2], function (newValue) { + translationIds.translate = newValue; + updateTranslations(); + }); + } + } else { + // do not assigne the translation id if it is empty. + translationIds.translate = !iElementText ? undefined : iElementText; + } + } else { + translationIds.translate = translationId; + } + updateTranslations(); + }; + + var observeAttributeTranslation = function (translateAttr) { + iAttr.$observe(translateAttr, function (translationId) { + translationIds[translateAttr] = translationId; + updateTranslations(); + }); + }; + + // initial setup with values + initInterpolationParams(scope.interpolateParams, iAttr, tAttr); + + var firstAttributeChangedEvent = true; + iAttr.$observe('translate', function (translationId) { + if (typeof translationId === 'undefined') { + // case of element "xyz" + observeElementTranslation(''); + } else { + // case of regular attribute + if (translationId !== '' || !firstAttributeChangedEvent) { + translationIds.translate = translationId; + updateTranslations(); + } + } + firstAttributeChangedEvent = false; + }); + + for (var translateAttr in iAttr) { + if (iAttr.hasOwnProperty(translateAttr) && translateAttr.substr(0, 13) === 'translateAttr') { + observeAttributeTranslation(translateAttr); + } + } + + iAttr.$observe('translateDefault', function (value) { + scope.defaultText = value; + updateTranslations(); + }); + + if (translateValuesExist) { + iAttr.$observe('translateValues', function (interpolateParams) { + if (interpolateParams) { + scope.$parent.$watch(function () { + angular.extend(scope.interpolateParams, $parse(interpolateParams)(scope.$parent)); + }); + } + }); + } + + if (translateValueExist) { + var observeValueAttribute = function (attrName) { + iAttr.$observe(attrName, function (value) { + var attributeName = angular.lowercase(attrName.substr(14, 1)) + attrName.substr(15); + scope.interpolateParams[attributeName] = value; + }); + }; + for (var attr in iAttr) { + if (Object.prototype.hasOwnProperty.call(iAttr, attr) && attr.substr(0, 14) === 'translateValue' && attr !== 'translateValues') { + observeValueAttribute(attr); + } + } + } + + // Master update function + var updateTranslations = function () { + for (var key in translationIds) { + if (translationIds.hasOwnProperty(key) && translationIds[key] !== undefined) { + updateTranslation(key, translationIds[key], scope, scope.interpolateParams, scope.defaultText, scope.translateNamespace); + } + } + }; + + // Put translation processing function outside loop + var updateTranslation = function(translateAttr, translationId, scope, interpolateParams, defaultTranslationText, translateNamespace) { + if (translationId) { + // if translation id starts with '.' and translateNamespace given, prepend namespace + if (translateNamespace && translationId.charAt(0) === '.') { + translationId = translateNamespace + translationId; + } + + $translate(translationId, interpolateParams, translateInterpolation, defaultTranslationText, scope.translateLanguage) + .then(function (translation) { + applyTranslation(translation, scope, true, translateAttr); + }, function (translationId) { + applyTranslation(translationId, scope, false, translateAttr); + }); + } else { + // as an empty string cannot be translated, we can solve this using successful=false + applyTranslation(translationId, scope, false, translateAttr); + } + }; + + var applyTranslation = function (value, scope, successful, translateAttr) { + if (!successful) { + if (typeof scope.defaultText !== 'undefined') { + value = scope.defaultText; + } + } + if (translateAttr === 'translate') { + // default translate into innerHTML + if (successful || (!successful && typeof iAttr.translateKeepContent === 'undefined')) { + iElement.empty().append(scope.preText + value + scope.postText); + } + var globallyEnabled = $translate.isPostCompilingEnabled(); + var locallyDefined = typeof tAttr.translateCompile !== 'undefined'; + var locallyEnabled = locallyDefined && tAttr.translateCompile !== 'false'; + if ((globallyEnabled && !locallyDefined) || locallyEnabled) { + $compile(iElement.contents())(scope); + } + } else { + // translate attribute + var attributeName = iAttr.$attr[translateAttr]; + if (attributeName.substr(0, 5) === 'data-') { + // ensure html5 data prefix is stripped + attributeName = attributeName.substr(5); + } + attributeName = attributeName.substr(15); + iElement.attr(attributeName, value); + } + }; + + if (translateValuesExist || translateValueExist || iAttr.translateDefault) { + scope.$watch('interpolateParams', updateTranslations, true); + } + + // Replaced watcher on translateLanguage with event listener + var unbindTranslateLanguage = 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); + + // ensure translation will be looked up at least one + if (iElement.text().length) { + if (iAttr.translate) { + observeElementTranslation(iAttr.translate); + } else { + observeElementTranslation(''); + } + } else if (iAttr.translate) { + // ensure attribute will be not skipped + observeElementTranslation(iAttr.translate); + } + updateTranslations(); + scope.$on('$destroy', function(){ + unbindTranslateLanguage(); + unbind(); + }); + }; + } + }; +} + +/** + * Returns the scope's namespace. + * @private + * @param scope + * @returns {string} + */ +function getTranslateNamespace(scope) { + 'use strict'; + if (scope.translateNamespace) { + return scope.translateNamespace; + } + if (scope.$parent) { + return getTranslateNamespace(scope.$parent); + } +} + +translateDirective.displayName = 'translateDirective'; + +angular.module('pascalprecht.translate') +/** + * @ngdoc directive + * @name pascalprecht.translate.directive:translateCloak + * @requires $rootScope + * @requires $translate + * @restrict A + * + * $description + * Adds a `translate-cloak` class name to the given element where this directive + * is applied initially and removes it, once a loader has finished loading. + * + * This directive can be used to prevent initial flickering when loading translation + * data asynchronously. + * + * The class name is defined in + * {@link pascalprecht.translate.$translateProvider#cloakClassName $translate.cloakClassName()}. + * + * @param {string=} translate-cloak If a translationId is provided, it will be used for showing + * or hiding the cloak. Basically it relies on the translation + * resolve. + */ +.directive('translateCloak', translateCloakDirective); + +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(); + + return function linkFn(scope, iElement, iAttr) { + 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); + }); + // Register for change events as this is being another indicicator revalidating the cloak) + $rootScope.$on('$translateChangeSuccess', function () { + $translate(iAttr.translateCloak).then(removeCloak, applyCloak); + }); + } + }; + } + }; +} + +translateCloakDirective.displayName = 'translateCloakDirective'; + +angular.module('pascalprecht.translate') +/** + * @ngdoc directive + * @name pascalprecht.translate.directive:translateNamespace + * @restrict A + * + * @description + * Translates given translation id either through attribute or DOM content. + * Internally it uses `translate` filter to translate translation id. It possible to + * pass an optional `translate-values` object literal as string into translation id. + * + * @param {string=} translate namespace name which could be either string or interpolated string. + * + * @example + + +
      + +
      +

      .HEADERS.TITLE

      +

      .HEADERS.WELCOME

      +
      + +
      +

      .TITLE

      +

      .WELCOME

      +
      + +
      +
      + + angular.module('ngView', ['pascalprecht.translate']) + + .config(function ($translateProvider) { + + $translateProvider.translations('en',{ + 'TRANSLATION_ID': 'Hello there!', + 'CONTENT': { + 'HEADERS': { + TITLE: 'Title' + } + }, + 'CONTENT.HEADERS.WELCOME': 'Welcome' + }).preferredLanguage('en'); + + }); + + +
      + */ +.directive('translateNamespace', translateNamespaceDirective); + +function translateNamespaceDirective() { + + 'use strict'; + + return { + restrict: 'A', + scope: true, + compile: function () { + return { + pre: function (scope, iElement, iAttrs) { + scope.translateNamespace = getTranslateNamespace(scope); + + if (scope.translateNamespace && iAttrs.translateNamespace.charAt(0) === '.') { + scope.translateNamespace += iAttrs.translateNamespace; + } else { + scope.translateNamespace = iAttrs.translateNamespace; + } + } + }; + } + }; +} + +/** + * Returns the scope's namespace. + * @private + * @param scope + * @returns {string} + */ +function getTranslateNamespace(scope) { + 'use strict'; + if (scope.translateNamespace) { + return scope.translateNamespace; + } + if (scope.$parent) { + return getTranslateNamespace(scope.$parent); + } +} + +translateNamespaceDirective.displayName = 'translateNamespaceDirective'; + +angular.module('pascalprecht.translate') +/** + * @ngdoc directive + * @name pascalprecht.translate.directive:translateLanguage + * @restrict A + * + * @description + * Forces the language to the directives in the underlying scope. + * + * @param {string=} translate language that will be negotiated. + * + * @example + + +
      + +
      +

      HELLO

      +
      + +
      +

      HELLO

      +
      + +
      +
      + + angular.module('ngView', ['pascalprecht.translate']) + + .config(function ($translateProvider) { + + $translateProvider + .translations('en',{ + 'HELLO': 'Hello world!' + }) + .translations('de',{ + 'HELLO': 'Hallo Welt!' + }) + .translations(.preferredLanguage('en'); + + }); + + +
      + */ +.directive('translateLanguage', translateLanguageDirective); + +function translateLanguageDirective() { + + 'use strict'; + + return { + restrict: 'A', + 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'); + }); + }; + } + }; +} + +translateLanguageDirective.displayName = 'translateLanguageDirective'; + +angular.module('pascalprecht.translate') +/** + * @ngdoc filter + * @name pascalprecht.translate.filter:translate + * @requires $parse + * @requires pascalprecht.translate.$translate + * @function + * + * @description + * Uses `$translate` service to translate contents. Accepts interpolate parameters + * to pass dynamized values though translation. + * + * @param {string} translationId A translation id to be translated. + * @param {*=} interpolateParams Optional object literal (as hash or string) to pass values into translation. + * + * @returns {string} Translated text. + * + * @example + + +
      + +
      {{ 'TRANSLATION_ID' | translate }}
      +
      {{ translationId | translate }}
      +
      {{ 'WITH_VALUES' | translate:'{value: 5}' }}
      +
      {{ 'WITH_VALUES' | translate:values }}
      + +
      +
      + + angular.module('ngView', ['pascalprecht.translate']) + + .config(function ($translateProvider) { + + $translateProvider.translations('en', { + 'TRANSLATION_ID': 'Hello there!', + 'WITH_VALUES': 'The following value is dynamic: {{value}}' + }); + $translateProvider.preferredLanguage('en'); + + }); + + angular.module('ngView').controller('TranslateCtrl', function ($scope) { + $scope.translationId = 'TRANSLATION_ID'; + + $scope.values = { + value: 78 + }; + }); + +
      + */ +.filter('translate', translateFilterFactory); + +function translateFilterFactory($parse, $translate) { + + 'use strict'; + + var translateFilter = function (translationId, interpolateParams, interpolation, forceLanguage) { + if (!angular.isObject(interpolateParams)) { + interpolateParams = $parse(interpolateParams)(this); + } + + return $translate.instant(translationId, interpolateParams, interpolation, forceLanguage); + }; + + if ($translate.statefulFilter()) { + translateFilter.$stateful = true; + } + + return translateFilter; +} + +translateFilterFactory.displayName = 'translateFilterFactory'; + +angular.module('pascalprecht.translate') + +/** + * @ngdoc object + * @name pascalprecht.translate.$translationCache + * @requires $cacheFactory + * + * @description + * The first time a translation table is used, it is loaded in the translation cache for quick retrieval. You + * can load translation tables directly into the cache by consuming the + * `$translationCache` service directly. + * + * @return {object} $cacheFactory object. + */ + .factory('$translationCache', $translationCache); + +function $translationCache($cacheFactory) { + + 'use strict'; + + return $cacheFactory('translations'); +} + +$translationCache.displayName = '$translationCache'; +return 'pascalprecht.translate'; + +})); + +/* +* AngularJs Fullcalendar Wrapper for the JQuery FullCalendar +* API @ http://arshaw.com/fullcalendar/ +* +* Angular Calendar Directive that takes in the [eventSources] nested array object as the ng-model and watches it deeply changes. +* Can also take in multiple event urls as a source object(s) and feed the events per view. +* The calendar will watch any eventSource array and update itself when a change is made. +* +*/ + +angular.module('ui.calendar', []) + .constant('uiCalendarConfig', {calendars: {}}) + .controller('uiCalendarCtrl', ['$scope', + '$locale', function( + $scope, + $locale){ + + var sources = $scope.eventSources, + extraEventSignature = $scope.calendarWatchEvent ? $scope.calendarWatchEvent : angular.noop, + + wrapFunctionWithScopeApply = function(functionToWrap){ + return function(){ + // This may happen outside of angular context, so create one if outside. + + if ($scope.$root.$$phase) { + return functionToWrap.apply(this, arguments); + } else { + var args = arguments; + var self = this; + return $scope.$root.$apply(function(){ + return functionToWrap.apply(self, args); + }); + } + }; + }; + + var eventSerialId = 1; + // @return {String} fingerprint of the event object and its properties + this.eventFingerprint = function(e) { + if (!e._id) { + e._id = eventSerialId++; + } + // This extracts all the information we need from the event. http://jsperf.com/angular-calendar-events-fingerprint/3 + return "" + e._id + (e.id || '') + (e.title || '') + (e.url || '') + (+e.start || '') + (+e.end || '') + + (e.allDay || '') + (e.className || '') + extraEventSignature({event: e}) || ''; + }; + + var sourceSerialId = 1, sourceEventsSerialId = 1; + // @return {String} fingerprint of the source object and its events array + this.sourceFingerprint = function(source) { + var fp = '' + (source.__id || (source.__id = sourceSerialId++)), + events = angular.isObject(source) && source.events; + if (events) { + fp = fp + '-' + (events.__id || (events.__id = sourceEventsSerialId++)); + } + return fp; + }; + + // @return {Array} all events from all sources + this.allEvents = function() { + // do sources.map(&:events).flatten(), but we don't have flatten + var arraySources = []; + for (var i = 0, srcLen = sources.length; i < srcLen; i++) { + var source = sources[i]; + if (angular.isArray(source)) { + // event source as array + arraySources.push(source); + } else if(angular.isObject(source) && angular.isArray(source.events)){ + // event source as object, ie extended form + var extEvent = {}; + for(var key in source){ + if(key !== '_id' && key !== 'events'){ + extEvent[key] = source[key]; + } + } + for(var eI = 0;eI < source.events.length;eI++){ + angular.extend(source.events[eI],extEvent); + } + arraySources.push(source.events); + } + } + return Array.prototype.concat.apply([], arraySources); + }; + + // Track changes in array of objects by assigning id tokens to each element and watching the scope for changes in the tokens + // @param {Array|Function} arraySource array of objects to watch + // @param tokenFn {Function} that returns the token for a given object + // @return {Object} + // subscribe: function(scope, function(newTokens, oldTokens)) + // called when source has changed. return false to prevent individual callbacks from firing + // onAdded/Removed/Changed: + // when set to a callback, called each item where a respective change is detected + this.changeWatcher = function(arraySource, tokenFn) { + var self; + var getTokens = function() { + var array = angular.isFunction(arraySource) ? arraySource() : arraySource; + var result = [], token, el; + for (var i = 0, n = array.length; i < n; i++) { + el = array[i]; + token = tokenFn(el); + map[token] = el; + result.push(token); + } + return result; + }; + + // @param {Array} a + // @param {Array} b + // @return {Array} elements in that are in a but not in b + // @example + // subtractAsSets([6, 100, 4, 5], [4, 5, 7]) // [6, 100] + var subtractAsSets = function(a, b) { + var result = [], inB = {}, i, n; + for (i = 0, n = b.length; i < n; i++) { + inB[b[i]] = true; + } + for (i = 0, n = a.length; i < n; i++) { + if (!inB[a[i]]) { + result.push(a[i]); + } + } + return result; + }; + + // Map objects to tokens and vice-versa + var map = {}; + + // Compare newTokens to oldTokens and call onAdded, onRemoved, and onChanged handlers for each affected event respectively. + var applyChanges = function(newTokens, oldTokens) { + var i, n, el, token; + var replacedTokens = {}; + var removedTokens = subtractAsSets(oldTokens, newTokens); + for (i = 0, n = removedTokens.length; i < n; i++) { + var removedToken = removedTokens[i]; + el = map[removedToken]; + delete map[removedToken]; + var newToken = tokenFn(el); + // if the element wasn't removed but simply got a new token, its old token will be different from the current one + if (newToken === removedToken) { + self.onRemoved(el); + } else { + replacedTokens[newToken] = removedToken; + self.onChanged(el); + } + } + + var addedTokens = subtractAsSets(newTokens, oldTokens); + for (i = 0, n = addedTokens.length; i < n; i++) { + token = addedTokens[i]; + el = map[token]; + if (!replacedTokens[token]) { + self.onAdded(el); + } + } + }; + return self = { + subscribe: function(scope, onArrayChanged) { + scope.$watch(getTokens, function(newTokens, oldTokens) { + var notify = !(onArrayChanged && onArrayChanged(newTokens, oldTokens) === false); + if (notify) { + applyChanges(newTokens, oldTokens); + } + }, true); + }, + onAdded: angular.noop, + onChanged: angular.noop, + onRemoved: angular.noop + }; + }; + + this.getFullCalendarConfig = function(calendarSettings, uiCalendarConfig){ + var config = {}; + + angular.extend(config, uiCalendarConfig); + angular.extend(config, calendarSettings); + + angular.forEach(config, function(value,key){ + if (typeof value === 'function'){ + config[key] = wrapFunctionWithScopeApply(config[key]); + } + }); + + return config; + }; + + this.getLocaleConfig = function(fullCalendarConfig) { + if (!fullCalendarConfig.lang || fullCalendarConfig.useNgLocale) { + // Configure to use locale names by default + var tValues = function(data) { + // convert {0: "Jan", 1: "Feb", ...} to ["Jan", "Feb", ...] + var r, k; + r = []; + for (k in data) { + r[k] = data[k]; + } + return r; + }; + var dtf = $locale.DATETIME_FORMATS; + return { + monthNames: tValues(dtf.MONTH), + monthNamesShort: tValues(dtf.SHORTMONTH), + dayNames: tValues(dtf.DAY), + dayNamesShort: tValues(dtf.SHORTDAY) + }; + } + return {}; + }; + }]) + .directive('uiCalendar', ['uiCalendarConfig', function(uiCalendarConfig) { + return { + restrict: 'A', + scope: {eventSources:'=ngModel',calendarWatchEvent: '&'}, + controller: 'uiCalendarCtrl', + link: function(scope, elm, attrs, controller) { + + var sources = scope.eventSources, + sourcesChanged = false, + calendar, + eventSourcesWatcher = controller.changeWatcher(sources, controller.sourceFingerprint), + eventsWatcher = controller.changeWatcher(controller.allEvents, controller.eventFingerprint), + options = null; + + function getOptions(){ + var calendarSettings = attrs.uiCalendar ? scope.$parent.$eval(attrs.uiCalendar) : {}, + fullCalendarConfig; + + fullCalendarConfig = controller.getFullCalendarConfig(calendarSettings, uiCalendarConfig); + + var localeFullCalendarConfig = controller.getLocaleConfig(fullCalendarConfig); + angular.extend(localeFullCalendarConfig, fullCalendarConfig); + options = { eventSources: sources }; + angular.extend(options, localeFullCalendarConfig); + //remove calendars from options + options.calendars = null; + + var options2 = {}; + for(var o in options){ + if(o !== 'eventSources'){ + options2[o] = options[o]; + } + } + return JSON.stringify(options2); + } + + scope.destroy = function(){ + if(calendar && calendar.fullCalendar){ + calendar.fullCalendar('destroy'); + } + if(attrs.calendar) { + calendar = uiCalendarConfig.calendars[attrs.calendar] = $(elm).html(''); + } else { + calendar = $(elm).html(''); + } + }; + + scope.init = function(){ + calendar.fullCalendar(options); + if(attrs.calendar) { + uiCalendarConfig.calendars[attrs.calendar] = calendar; + } + }; + + eventSourcesWatcher.onAdded = function(source) { + calendar.fullCalendar('addEventSource', source); + sourcesChanged = true; + }; + + eventSourcesWatcher.onRemoved = function(source) { + calendar.fullCalendar('removeEventSource', source); + sourcesChanged = true; + }; + + eventSourcesWatcher.onChanged = function(source) { + calendar.fullCalendar('refetchEvents'); + sourcesChanged = true; + }; + + eventsWatcher.onAdded = function(event) { + calendar.fullCalendar('renderEvent', event, (event.stick ? true : false)); + }; + + eventsWatcher.onRemoved = function(event) { + calendar.fullCalendar('removeEvents', event._id); + }; + + eventsWatcher.onChanged = function(event) { + var clientEvents = calendar.fullCalendar('clientEvents', event._id); + for (var i = 0; i < clientEvents.length; i++) { + var clientEvent = clientEvents[i]; + clientEvent = angular.extend(clientEvent, event); + calendar.fullCalendar('updateEvent', clientEvent); + } + }; + + eventSourcesWatcher.subscribe(scope); + eventsWatcher.subscribe(scope, function() { + if (sourcesChanged === true) { + sourcesChanged = false; + // return false to prevent onAdded/Removed/Changed handlers from firing in this case + return false; + } + }); + + scope.$watch(getOptions, function(newO,oldO){ + scope.destroy(); + scope.init(); + }); + } + }; +}]); + +(function() { + 'use strict'; + + /** + * Usage pattern + * + */ + var days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; + var months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; + + angular.module('ds.clock', []) + .directive('dsWidgetClock', ['$interval', '$filter', + function($interval, $filter) { + return clock($interval, $filter); + } + ]); + + function clock($interval, $filter) { + return { + restrict: 'EA', + scope: { + gmtOffset: '=gmtOffset', + digitalFormat: '=digitalFormat', + showDigital: '=showDigital', + showAnalog: '=showAnalog', + startTime: '=startTime', + theme: '=theme' + }, + template: '
      {{digital}}
      {{gmtInfo}}
      ', + link: function(scope, element, attrs) { + var format, // date format + stopTime; // so that we can cancel the time updates + var o = {}; + var gmtOffset = scope.gmtOffset; + var digitalFormat = scope.digitalFormat ? scope.digitalFormat : 'HH:mm:ss'; + o.showDigital = scope.showDigital != null ? scope.showDigital : attrs.showDigital !== undefined ? true : false; + o.showAnalog = scope.showAnalog != null ? scope.showAnalog : attrs.showAnalog !== undefined ? true : false; + o.showGmtInfo = attrs.showGmtInfo !== undefined ? true : false; + o.startTime = parseInt(scope.startTime, 10); // ms + scope.themeClass = scope.theme ? scope.theme : attrs.theme ? attrs.theme : 'light'; + if (!o.showDigital && !o.showAnalog) { + o.showAnalog = true; + o.showDigital = true; + } + scope.gmtInfo = false; + + scope.date = getDate(o); + + scope.digital = o.showDigital ? 'Loading..' : false; + scope.analog = o.showAnalog; + scope.majors = new Array(12); + scope.minors = new Array(60); + var date = null; + var tick = function() { + if (!isNaN(o.startTime)) { + o.startTime = o.startTime + 1000; + } + date = getDate(o); + scope.date = date; + if (o.showDigital) { + scope.digital = timeText(date, digitalFormat, gmtOffset, $filter); + } + }; + + stopTime = $interval(tick, 1000); + // watch the expression, and update the UI on change. + scope.$watch('gmtOffset', function(value, old) { + + gmtOffset = value; + o.gmtOffset = (gmtOffset != null) ? getGMTbase100(gmtOffset) : false; + if (o.showGmtInfo && o.gmtOffset !== false) { + scope.gmtInfo = getGMTText(o.gmtOffset); + } + + tick(); + }); + scope.$watch('digitalFormat', function(value, old) { + if(value != old){ + digitalFormat = value; + } + }); + scope.$watch('showDigital', function(value, old) { + if(value != old){ + o.showDigital = value; + scope.digital = o.showDigital ? 'Loading..' : false;; + } + }); + scope.$watch('showAnalog', function(value, old) { + if(value != old){ + o.showAnalog = value; + scope.analog = value; + } + }); + scope.$watch('theme', function(value, old) { + if(value != old){ + scope.themeClass = value ? value : attrs.theme ? attrs.theme : 'light'; + } + }); + // listen on DOM destroy (removal) event, and cancel the next UI update + // to prevent updating time after the DOM element was removed. + element.on('$destroy', function() { + $interval.cancel(stopTime); + stopTime = null; + }); + + } + }; + } + + function getGMTbase100(offset) { + offset = parseFloat(offset); + var f = offset > 0 ? Math.floor(offset) : Math.ceil(offset), + s = (offset % 1) / 0.6; + + return f + s; + + } + + function getGMTbase60(offset) { + var f = offset > 0 ? Math.floor(offset) : Math.ceil(offset), + s = ((offset > 0 ? offset : offset * -1) % 1) * 60; + return f + s; + + } + + function getGMTText(offset) { + + var f = offset > 0 ? Math.floor(offset) : Math.ceil(offset), + s = Math.round(((offset > 0 ? offset : offset * -1) % 1) * 60); + + return 'GMT' + (offset === 0 ? '' : ((offset > 0 ? ' +' : ' ') + lpad(f) + '.' + rpad(s).substring(0, 2))); + + } + + function lpad(num) { + if (num < 0) { + return (num > -10 ? '-0' : '-') + (num * -1); + } else { + return (num < 10 ? '0' : '') + num; + } + + } + + function rpad(num) { + return num + (num < 10 ? '0' : ''); + } + // Checkfor offset and get correct time + function getDate(o) { + var now = (!isNaN(o.startTime)) ? new Date(o.startTime) : new Date(); + if (o.gmtOffset !== null && o.gmtOffset !== false) { + /*Use GMT + gmtOffset + convert to msec + add local time zone offset + get UTC time in msec*/ + var utc = now.getTime() + (now.getTimezoneOffset() * 60000); + // create new Date object for different city + // using supplied offset + var offsetNow = new Date(utc + (3600000 * o.gmtOffset)); + return { + hrs: offsetNow.getHours(), + mins: offsetNow.getMinutes(), + secs: offsetNow.getSeconds(), + date: offsetNow + }; + } else { + // Use local time + return { + hrs: now.getHours(), + mins: now.getMinutes(), + secs: now.getSeconds(), + date: now + }; + } + } + + function timeText(d, format, timezone, $filter) { + return $filter('date')(d.date, format, timezone); + } + +})(); + +/** + * State-based routing for AngularJS + * @version v0.3.0 + * @link http://angular-ui.github.com/ + * @license MIT License, http://www.opensource.org/licenses/MIT + */ + +/* commonjs package manager support (eg componentjs) */ +if (typeof module !== "undefined" && typeof exports !== "undefined" && module.exports === exports){ + module.exports = 'ui.router'; +} + +(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, + toJson = angular.toJson; + +function inherit(parent, extra) { + return extend(new (extend(function() {}, { prototype: parent }))(), extra); +} + +function merge(dst) { + forEach(arguments, function(obj) { + if (obj !== dst) { + forEach(obj, function(value, key) { + if (!dst.hasOwnProperty(key)) dst[key] = value; + }); + } + }); + return dst; +} + +/** + * Finds the common ancestor path between two states. + * + * @param {Object} first The first state. + * @param {Object} second The second state. + * @return {Array} Returns an array of state names in descending order, not including the root. + */ +function ancestors(first, second) { + var path = []; + + for (var n in first.path) { + if (first.path[n] !== second.path[n]) break; + path.push(first.path[n]); + } + return path; +} + +/** + * IE8-safe wrapper for `Object.keys()`. + * + * @param {Object} object A JavaScript object. + * @return {Array} Returns the keys of the object as an array. + */ +function objectKeys(object) { + if (Object.keys) { + return Object.keys(object); + } + var result = []; + + forEach(object, function(val, key) { + result.push(key); + }); + 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 indexOf(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; +} + +/** + * Merges a set of parameters with all parameters inherited between the common parents of the + * current state and a given destination state. + * + * @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. + */ +function inheritParams(currentParams, newParams, $current, $to) { + var parents = ancestors($current, $to), parentParams, inherited = {}, inheritList = []; + + for (var i in parents) { + if (!parents[i] || !parents[i].params) continue; + parentParams = objectKeys(parents[i].params); + if (!parentParams.length) continue; + + for (var j in parentParams) { + if (indexOf(inheritList, parentParams[j]) >= 0) continue; + inheritList.push(parentParams[j]); + inherited[parentParams[j]] = currentParams[parentParams[j]]; + } + } + return extend({}, inherited, newParams); +} + +/** + * 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`. + */ +function equalForKeys(a, b, keys) { + if (!keys) { + keys = []; + for (var n in a) keys.push(n); // Used instead of Object.keys() for IE8 compatibility + } + + for (var i=0; i + * + * + * + * + * + * + * + * + * + * + * + * + */ +angular.module('ui.router', ['ui.router.state']); + +angular.module('ui.router.compat', ['ui.router']); + +/** + * @ngdoc object + * @name ui.router.util.$resolve + * + * @requires $q + * @requires $injector + * + * @description + * Manages resolution of (acyclic) graphs of promises. + */ +$Resolve.$inject = ['$q', '$injector']; +function $Resolve( $q, $injector) { + + var VISIT_IN_PROGRESS = 1, + VISIT_DONE = 2, + NOTHING = {}, + NO_DEPENDENCIES = [], + NO_LOCALS = NOTHING, + NO_PARENT = extend($q.when(NOTHING), { $$promises: NOTHING, $$values: NOTHING }); + + + /** + * @ngdoc function + * @name ui.router.util.$resolve#study + * @methodOf ui.router.util.$resolve + * + * @description + * Studies a set of invocables that are likely to be used multiple times. + *
      +   * $resolve.study(invocables)(locals, parent, self)
      +   * 
      + * is equivalent to + *
      +   * $resolve.resolve(invocables, locals, parent, self)
      +   * 
      + * but the former is more efficient (in fact `resolve` just calls `study` + * internally). + * + * @param {object} invocables Invocable objects + * @return {function} a function to pass in locals, parent and self + */ + this.study = function (invocables) { + if (!isObject(invocables)) throw new Error("'invocables' must be an object"); + var invocableKeys = objectKeys(invocables || {}); + + // Perform a topological sort of invocables to build an ordered plan + var plan = [], cycle = [], visited = {}; + function visit(value, key) { + if (visited[key] === VISIT_DONE) return; + + cycle.push(key); + if (visited[key] === VISIT_IN_PROGRESS) { + cycle.splice(0, indexOf(cycle, key)); + throw new Error("Cyclic dependency: " + cycle.join(" -> ")); + } + 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; + } + 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 = result.$$promises || true; // keep for isResolve() + delete result.$$inheritedValues; + 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; + } + + if (parent.$$inheritedValues) { + merge(values, omit(parent.$$inheritedValues, invocableKeys)); + } + + // Merge parent values if the parent has already resolved, or merge + // parent promises and wait if the parent resolve is still in progress. + extend(promises, parent.$$promises); + if (parent.$$values) { + merged = merge(values, omit(parent.$$values, invocableKeys)); + result.$$inheritedValues = omit(parent.$$values, invocableKeys); + done(); + } else { + if (parent.$$inheritedValues) { + result.$$inheritedValues = omit(parent.$$inheritedValues, invocableKeys); + } + 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} 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, headers: { Accept: 'text/html' }}) + .then(function(response) { return response.data; }); + }; + + /** + * @ngdoc function + * @name ui.router.util.$templateFactory#fromProvider + * @methodOf ui.router.util.$templateFactory + * + * @description + * Creates a template by invoking an injectable provider function. + * + * @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 }); + }; +} + +angular.module('ui.router.util').service('$templateFactory', $TemplateFactory); + +var $$UMFP; // reference to $UrlMatcherFactoryProvider + +/** + * @ngdoc object + * @name ui.router.util.type:UrlMatcher + * + * @description + * 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 ui.router.util.type:UrlMatcher#methods_exec exec}. + * + * 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: + * + * * `':'` name - colon placeholder + * * `'*'` name - catch-all placeholder + * * `'{' name '}'` - curly placeholder + * * `'{' name ':' regexp|type '}'` - curly placeholder with regexp or type name. Should the + * regexp itself contain curly braces, they must be in matched pairs or escaped with a backslash. + * + * 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. + * + * 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` Type matches `2014-11-12`) and provides a Date object in $stateParams.start + * + * @param {string} pattern The pattern to compile into a matcher. + * @param {Object} config A configuration object hash: + * @param {Object=} parentMatcher Used to concatenate the pattern/config onto + * an existing UrlMatcher + * + * * `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`. + * + * @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 ui.router.util.type:UrlMatcher#methods_exec exec()} returns + * non-null) will start with this prefix. + * + * @property {string} source The pattern that was passed into the constructor + * + * @property {string} sourcePath The path portion of the source property + * + * @property {string} sourceSearch The search portion of the source property + * + * @property {string} regex The constructed regex that will be used to match against the url when + * it is time to determine which url will match. + * + * @returns {Object} New `UrlMatcher` object + */ +function UrlMatcher(pattern, config, parentMatcher) { + config = extend({ params: {} }, isObject(config) ? config : {}); + + // 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, + compiled = '^', last = 0, m, + segments = this.segments = [], + parentParams = parentMatcher ? parentMatcher.params : {}, + params = this.params = parentMatcher ? parentMatcher.params.$$new() : new $$UMFP.ParamSet(), + paramNames = []; + + function addParameter(id, type, config, location) { + paramNames.push(id); + if (parentParams[id]) return parentParams[id]; + if (!/^\w+([-.]+\w+)*(?:\[\])?$/.test(id)) throw new Error("Invalid parameter name '" + id + "' in pattern '" + pattern + "'"); + if (params[id]) throw new Error("Duplicate parameter name '" + id + "' in pattern '" + pattern + "'"); + params[id] = new $$UMFP.Param(id, type, config, location); + return params[id]; + } + + function quoteRegExp(string, pattern, squash, optional) { + var surroundPattern = ['',''], result = string.replace(/[\\\[\]\^$*+?.()|{}]/g, "\\$&"); + if (!pattern) return result; + switch(squash) { + case false: surroundPattern = ['(', ')' + (optional ? "?" : "")]; break; + case true: + result = result.replace(/\/$/, ''); + surroundPattern = ['(?:\/(', ')|\/)?']; + break; + default: surroundPattern = ['(' + squash + "|", ')?']; break; + } + return result + surroundPattern[0] + pattern + surroundPattern[1]; + } + + 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. + function matchDetails(m, isSearch) { + var id, regexp, segment, type, cfg, arrayMode; + id = m[2] || m[3]; // IE[78] returns '' for unmatched groups instead of null + cfg = config.params[id]; + segment = pattern.substring(last, m.index); + regexp = isSearch ? m[4] : m[4] || (m[1] == '*' ? '.*' : null); + + if (regexp) { + type = $$UMFP.type(regexp) || inherit($$UMFP.type("string"), { pattern: new RegExp(regexp, config.caseInsensitive ? 'i' : undefined) }); + } + + return { + id: id, regexp: regexp, segment: segment, type: type, cfg: cfg + }; + } + + var p, param, segment; + while ((m = placeholder.exec(pattern))) { + p = matchDetails(m, false); + if (p.segment.indexOf('?') >= 0) break; // we're into the search part + + param = addParameter(p.id, p.type, p.cfg, "path"); + compiled += quoteRegExp(p.segment, param.type.pattern.source, param.squash, param.isOptional); + segments.push(p.segment); + last = placeholder.lastIndex; + } + segment = pattern.substring(last); + + // 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); + + if (search.length > 0) { + last = 0; + while ((m = searchPlaceholder.exec(search))) { + p = matchDetails(m, true); + param = addParameter(p.id, p.type, p.cfg, "search"); + last = placeholder.lastIndex; + // check if ?& + } + } + } else { + this.sourcePath = pattern; + this.sourceSearch = ''; + } + + compiled += quoteRegExp(segment) + (config.strict === false ? '\/?' : '') + '$'; + segments.push(segment); + + this.regexp = new RegExp(compiled, config.caseInsensitive ? 'i' : undefined); + this.prefix = segments[0]; + this.$$paramNames = paramNames; +} + +/** + * @ngdoc function + * @name ui.router.util.type:UrlMatcher#concat + * @methodOf ui.router.util.type:UrlMatcher + * + * @description + * 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. + * @param {Object} config An object hash of the configuration for the matcher. + * @returns {UrlMatcher} A matcher for the concatenated pattern. + */ +UrlMatcher.prototype.concat = function (pattern, config) { + // 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. + var defaultConfig = { + caseInsensitive: $$UMFP.caseInsensitive(), + strict: $$UMFP.strictMode(), + squash: $$UMFP.defaultSquashPolicy() + }; + return new UrlMatcher(this.sourcePath + pattern + this.sourceSearch, extend(defaultConfig, config), this); +}; + +UrlMatcher.prototype.toString = function () { + return this.source; +}; + +/** + * @ngdoc function + * @name ui.router.util.type:UrlMatcher#exec + * @methodOf ui.router.util.type:UrlMatcher + * + * @description + * 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. + * + * @example + *
      + * new UrlMatcher('/user/{id}?q&r').exec('/user/bob', {
      + *   x: '1', q: 'hello'
      + * });
      + * // returns { id: 'bob', q: 'hello', r: null }
      + * 
      + * + * @param {string} path The URL path to match, e.g. `$location.path()`. + * @param {Object} searchParams URL search parameters, e.g. `$location.search()`. + * @returns {Object} The captured parameter values. + */ +UrlMatcher.prototype.exec = function (path, searchParams) { + var m = this.regexp.exec(path); + if (!m) return null; + searchParams = searchParams || {}; + + var paramNames = this.parameters(), nTotal = paramNames.length, + nPath = this.segments.length - 1, + values = {}, i, j, cfg, paramName; + + if (nPath !== m.length - 1) throw new Error("Unbalanced capture group in route '" + this.source + "'"); + + function decodePathArray(string) { + function reverseString(str) { return str.split("").reverse().join(""); } + function unquoteDashes(str) { return str.replace(/\\-/g, "-"); } + + var split = reverseString(string).split(/-(?!\\)/); + var allReversed = map(split, reverseString); + return map(allReversed, unquoteDashes).reverse(); + } + + var param, paramVal; + for (i = 0; i < nPath; i++) { + paramName = paramNames[i]; + param = this.params[paramName]; + paramVal = m[i+1]; + // if the param value matches a pre-replace pair, replace the value before decoding. + for (j = 0; j < param.replace.length; j++) { + if (param.replace[j].from === paramVal) paramVal = param.replace[j].to; + } + if (paramVal && param.array === true) paramVal = decodePathArray(paramVal); + if (isDefined(paramVal)) paramVal = param.type.decode(paramVal); + values[paramName] = param.value(paramVal); + } + for (/**/; i < nTotal; i++) { + paramName = paramNames[i]; + values[paramName] = this.params[paramName].value(searchParams[paramName]); + param = this.params[paramName]; + paramVal = searchParams[paramName]; + for (j = 0; j < param.replace.length; j++) { + if (param.replace[j].from === paramVal) paramVal = param.replace[j].to; + } + if (isDefined(paramVal)) paramVal = param.type.decode(paramVal); + values[paramName] = param.value(paramVal); + } + + return values; +}; + +/** + * @ngdoc function + * @name ui.router.util.type:UrlMatcher#parameters + * @methodOf ui.router.util.type:UrlMatcher + * + * @description + * Returns the names of all path and search parameters of this pattern in an unspecified order. + * + * @returns {Array.} 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 (param) { + if (!isDefined(param)) return this.$$paramNames; + return this.params[param] || null; +}; + +/** + * @ngdoc function + * @name ui.router.util.type:UrlMatcher#validates + * @methodOf ui.router.util.type:UrlMatcher + * + * @description + * Checks an object hash of parameters to validate their correctness according to the parameter + * types of this `UrlMatcher`. + * + * @param {Object} params The object hash of parameters to validate. + * @returns {boolean} Returns `true` if `params` validates, otherwise `false`. + */ +UrlMatcher.prototype.validates = function (params) { + return this.params.$$validates(params); +}; + +/** + * @ngdoc function + * @name ui.router.util.type:UrlMatcher#format + * @methodOf ui.router.util.type:UrlMatcher + * + * @description + * 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. + * @returns {string} the formatted URL (path and optionally search part). + */ +UrlMatcher.prototype.format = function (values) { + values = values || {}; + var segments = this.segments, params = this.parameters(), paramset = this.params; + if (!this.validates(values)) return null; + + var i, search = false, nPath = segments.length - 1, nTotal = params.length, result = segments[0]; + + function encodeDashes(str) { // Replace dashes with encoded "\-" + return encodeURIComponent(str).replace(/-/g, function(c) { return '%5C%' + c.charCodeAt(0).toString(16).toUpperCase(); }); + } + + for (i = 0; i < nTotal; i++) { + var isPathParam = i < nPath; + var name = params[i], param = paramset[name], value = param.value(values[name]); + var isDefaultValue = param.isOptional && param.type.equals(param.value(), value); + var squash = isDefaultValue ? param.squash : false; + var encoded = param.type.encode(value); + + if (isPathParam) { + var nextSegment = segments[i + 1]; + var isFinalPathParam = i + 1 === nPath; + + if (squash === false) { + if (encoded != null) { + if (isArray(encoded)) { + result += map(encoded, encodeDashes).join("-"); + } else { + result += encodeURIComponent(encoded); + } + } + result += nextSegment; + } else if (squash === true) { + var capture = result.match(/\/$/) ? /\/?(.*)/ : /(.*)/; + result += nextSegment.match(capture)[1]; + } else if (isString(squash)) { + result += squash + nextSegment; + } + + if (isFinalPathParam && param.squash === true && result.slice(-1) === '/') result = result.slice(0, -1); + } else { + if (encoded == null || (isDefaultValue && squash !== false)) continue; + if (!isArray(encoded)) encoded = [ encoded ]; + if (encoded.length === 0) continue; + encoded = map(encoded, encodeURIComponent).join('&' + name + '='); + result += (search ? '&' : '?') + (name + '=' + encoded); + search = true; + } + } + + return result; +}; + +/** + * @ngdoc object + * @name ui.router.util.type:Type + * + * @description + * Implements an interface to define custom parameter types that can be decoded from and encoded to + * string parameters matched in a URL. Used by {@link ui.router.util.type:UrlMatcher `UrlMatcher`} + * objects when matching or formatting URLs, or comparing or validating parameter values. + * + * See {@link ui.router.util.$urlMatcherFactory#methods_type `$urlMatcherFactory#type()`} for more + * information on registering custom types. + * + * @param {Object} config A configuration object which contains the custom type definition. The object's + * properties will override the default methods and/or pattern in `Type`'s public interface. + * @example + *
      + * {
      + *   decode: function(val) { return parseInt(val, 10); },
      + *   encode: function(val) { return val && val.toString(); },
      + *   equals: function(a, b) { return this.is(a) && a === b; },
      + *   is: function(val) { return angular.isNumber(val) isFinite(val) && val % 1 === 0; },
      + *   pattern: /\d+/
      + * }
      + * 
      + * + * @property {RegExp} pattern The regular expression pattern used to match values of this type when + * coming from a substring of a URL. + * + * @returns {Object} Returns a new `Type` object. + */ +function Type(config) { + extend(this, config); +} + +/** + * @ngdoc function + * @name ui.router.util.type:Type#is + * @methodOf ui.router.util.type:Type + * + * @description + * Detects whether a value is of a particular type. Accepts a native (decoded) value + * and determines whether it matches the current `Type` object. + * + * @param {*} val The value to check. + * @param {string} key Optional. If the type check is happening in the context of a specific + * {@link ui.router.util.type:UrlMatcher `UrlMatcher`} object, this is the name of the + * parameter in which `val` is stored. Can be used for meta-programming of `Type` objects. + * @returns {Boolean} Returns `true` if the value matches the type, otherwise `false`. + */ +Type.prototype.is = function(val, key) { + return true; +}; + +/** + * @ngdoc function + * @name ui.router.util.type:Type#encode + * @methodOf ui.router.util.type:Type + * + * @description + * Encodes a custom/native type value to a string that can be embedded in a URL. Note that the + * return value does *not* need to be URL-safe (i.e. passed through `encodeURIComponent()`), it + * only needs to be a representation of `val` that has been coerced to a string. + * + * @param {*} val The value to encode. + * @param {string} key The name of the parameter in which `val` is stored. Can be used for + * meta-programming of `Type` objects. + * @returns {string} Returns a string representation of `val` that can be encoded in a URL. + */ +Type.prototype.encode = function(val, key) { + return val; +}; + +/** + * @ngdoc function + * @name ui.router.util.type:Type#decode + * @methodOf ui.router.util.type:Type + * + * @description + * Converts a parameter value (from URL string or transition param) to a custom/native value. + * + * @param {string} val The URL parameter value to decode. + * @param {string} key The name of the parameter in which `val` is stored. Can be used for + * meta-programming of `Type` objects. + * @returns {*} Returns a custom representation of the URL parameter value. + */ +Type.prototype.decode = function(val, key) { + return val; +}; + +/** + * @ngdoc function + * @name ui.router.util.type:Type#equals + * @methodOf ui.router.util.type:Type + * + * @description + * Determines whether two decoded values are equivalent. + * + * @param {*} a A value to compare against. + * @param {*} b A value to compare against. + * @returns {Boolean} Returns `true` if the values are equivalent/equal, otherwise `false`. + */ +Type.prototype.equals = function(a, b) { + return a == b; +}; + +Type.prototype.$subPattern = function() { + var sub = this.pattern.toString(); + return sub.substr(1, sub.length - 2); +}; + +Type.prototype.pattern = /.*/; + +Type.prototype.toString = function() { return "{Type:" + this.name + "}"; }; + +/** Given an encoded string, or a decoded object, returns a decoded object */ +Type.prototype.$normalize = function(val) { + return this.is(val) ? val : this.decode(val); +}; + +/* + * Wraps an existing custom Type as an array of Type, depending on 'mode'. + * e.g.: + * - urlmatcher pattern "/path?{queryParam[]:int}" + * - url: "/path?queryParam=1&queryParam=2 + * - $stateParams.queryParam will be [1, 2] + * if `mode` is "auto", then + * - url: "/path?queryParam=1 will create $stateParams.queryParam: 1 + * - url: "/path?queryParam=1&queryParam=2 will create $stateParams.queryParam: [1, 2] + */ +Type.prototype.$asArray = function(mode, isSearch) { + if (!mode) return this; + if (mode === "auto" && !isSearch) throw new Error("'auto' array mode is for query parameters only"); + + function ArrayType(type, mode) { + function bindTo(type, callbackName) { + return function() { + return type[callbackName].apply(type, arguments); + }; + } + + // Wrap non-array value as array + function arrayWrap(val) { return isArray(val) ? val : (isDefined(val) ? [ val ] : []); } + // Unwrap array value for "auto" mode. Return undefined for empty array. + function arrayUnwrap(val) { + switch(val.length) { + case 0: return undefined; + case 1: return mode === "auto" ? val[0] : val; + default: return val; + } + } + function falsey(val) { return !val; } + + // Wraps type (.is/.encode/.decode) functions to operate on each value of an array + function arrayHandler(callback, allTruthyMode) { + return function handleArray(val) { + if (isArray(val) && val.length === 0) return val; + val = arrayWrap(val); + var result = map(val, callback); + if (allTruthyMode === true) + return filter(result, falsey).length === 0; + return arrayUnwrap(result); + }; + } + + // Wraps type (.equals) functions to operate on each value of an array + function arrayEqualsHandler(callback) { + return function handleArray(val1, val2) { + var left = arrayWrap(val1), right = arrayWrap(val2); + if (left.length !== right.length) return false; + for (var i = 0; i < left.length; i++) { + if (!callback(left[i], right[i])) return false; + } + return true; + }; + } + + this.encode = arrayHandler(bindTo(type, 'encode')); + this.decode = arrayHandler(bindTo(type, 'decode')); + this.is = arrayHandler(bindTo(type, 'is'), true); + this.equals = arrayEqualsHandler(bindTo(type, 'equals')); + this.pattern = type.pattern; + this.$normalize = arrayHandler(bindTo(type, '$normalize')); + this.name = type.name; + this.$arrayMode = mode; + } + + return new ArrayType(this, mode); +}; + + + +/** + * @ngdoc object + * @name ui.router.util.$urlMatcherFactory + * + * @description + * Factory for {@link ui.router.util.type:UrlMatcher `UrlMatcher`} instances. The factory + * is also available to providers under the name `$urlMatcherFactoryProvider`. + */ +function $UrlMatcherFactory() { + $$UMFP = this; + + var isCaseInsensitive = false, isStrictMode = true, defaultSquashPolicy = false; + + // Use tildes to pre-encode slashes. + // If the slashes are simply URLEncoded, the browser can choose to pre-decode them, + // and bidirectional encoding/decoding fails. + // Tilde was chosen because it's not a RFC 3986 section 2.2 Reserved Character + function valToString(val) { return val != null ? val.toString().replace(/~/g, "~~").replace(/\//g, "~2F") : val; } + function valFromString(val) { return val != null ? val.toString().replace(/~2F/g, "/").replace(/~~/g, "~") : val; } + + var $types = {}, enqueue = true, typeQueue = [], injector, defaultTypes = { + "string": { + encode: valToString, + decode: valFromString, + // TODO: in 1.0, make string .is() return false if value is undefined/null by default. + // In 0.2.x, string params are optional by default for backwards compat + is: function(val) { return val == null || !isDefined(val) || typeof val === "string"; }, + pattern: /[^/]*/ + }, + "int": { + encode: valToString, + decode: function(val) { return parseInt(val, 10); }, + is: function(val) { return isDefined(val) && this.decode(val.toString()) === val; }, + pattern: /\d+/ + }, + "bool": { + encode: function(val) { return val ? 1 : 0; }, + decode: function(val) { return parseInt(val, 10) !== 0; }, + is: function(val) { return val === true || val === false; }, + pattern: /0|1/ + }, + "date": { + encode: function (val) { + if (!this.is(val)) + return undefined; + return [ val.getFullYear(), + ('0' + (val.getMonth() + 1)).slice(-2), + ('0' + val.getDate()).slice(-2) + ].join("-"); + }, + decode: function (val) { + if (this.is(val)) return val; + var match = this.capture.exec(val); + return match ? new Date(match[1], match[2] - 1, match[3]) : undefined; + }, + is: function(val) { return val instanceof Date && !isNaN(val.valueOf()); }, + equals: function (a, b) { return this.is(a) && this.is(b) && a.toISOString() === b.toISOString(); }, + 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": { + encode: angular.toJson, + decode: angular.fromJson, + is: angular.isObject, + equals: angular.equals, + pattern: /[^/]*/ + }, + "any": { // does not encode/decode + encode: angular.identity, + decode: angular.identity, + equals: angular.equals, + pattern: /.*/ + } + }; + + function getDefaultConfig() { + return { + strict: isStrictMode, + caseInsensitive: isCaseInsensitive + }; + } + + function isInjectable(value) { + return (isFunction(value) || (isArray(value) && isFunction(value[value.length - 1]))); + } + + /** + * [Internal] Get the default value of a parameter, which may be an injectable function. + */ + $UrlMatcherFactory.$$getDefaultValue = function(config) { + if (!isInjectable(config.value)) return config.value; + if (!injector) throw new Error("Injectable functions cannot be called at configuration time"); + return injector.invoke(config.value); + }; + + /** + * @ngdoc function + * @name ui.router.util.$urlMatcherFactory#caseInsensitive + * @methodOf ui.router.util.$urlMatcherFactory + * + * @description + * Defines whether URL matching should be case sensitive (the default behavior), or not. + * + * @param {boolean} value `false` to match URL in a case sensitive manner; otherwise `true`; + * @returns {boolean} the current value of caseInsensitive + */ + this.caseInsensitive = function(value) { + if (isDefined(value)) + isCaseInsensitive = value; + return isCaseInsensitive; + }; + + /** + * @ngdoc function + * @name ui.router.util.$urlMatcherFactory#strictMode + * @methodOf ui.router.util.$urlMatcherFactory + * + * @description + * Defines whether URLs should match trailing slashes, or not (the default behavior). + * + * @param {boolean=} value `false` to match trailing slashes in URLs, otherwise `true`. + * @returns {boolean} the current value of strictMode + */ + this.strictMode = function(value) { + if (isDefined(value)) + isStrictMode = value; + return isStrictMode; + }; + + /** + * @ngdoc function + * @name ui.router.util.$urlMatcherFactory#defaultSquashPolicy + * @methodOf ui.router.util.$urlMatcherFactory + * + * @description + * Sets the default behavior when generating or matching URLs with default parameter values. + * + * @param {string} value A string that defines the default parameter URL squashing behavior. + * `nosquash`: When generating an href with a default parameter value, do not squash the parameter value from the URL + * `slash`: When generating an href with a default parameter value, squash (remove) the parameter value, and, if the + * parameter is surrounded by slashes, squash (remove) one slash from the URL + * any other string, e.g. "~": When generating an href with a default parameter value, squash (remove) + * the parameter value from the URL and replace it with this string. + */ + this.defaultSquashPolicy = function(value) { + if (!isDefined(value)) return defaultSquashPolicy; + if (value !== true && value !== false && !isString(value)) + throw new Error("Invalid squash policy: " + value + ". Valid policies: false, true, arbitrary-string"); + defaultSquashPolicy = value; + return value; + }; + + /** + * @ngdoc function + * @name ui.router.util.$urlMatcherFactory#compile + * @methodOf ui.router.util.$urlMatcherFactory + * + * @description + * Creates a {@link ui.router.util.type:UrlMatcher `UrlMatcher`} for the specified pattern. + * + * @param {string} pattern The URL pattern. + * @param {Object} config The config object hash. + * @returns {UrlMatcher} The UrlMatcher. + */ + this.compile = function (pattern, config) { + return new UrlMatcher(pattern, extend(getDefaultConfig(), config)); + }; + + /** + * @ngdoc function + * @name ui.router.util.$urlMatcherFactory#isMatcher + * @methodOf ui.router.util.$urlMatcherFactory + * + * @description + * Returns true if the specified object is a `UrlMatcher`, or false otherwise. + * + * @param {Object} object The object to perform the type check against. + * @returns {Boolean} Returns `true` if the object matches the `UrlMatcher` interface, by + * implementing all the same methods. + */ + this.isMatcher = function (o) { + if (!isObject(o)) return false; + var result = true; + + forEach(UrlMatcher.prototype, function(val, name) { + if (isFunction(val)) { + result = result && (isDefined(o[name]) && isFunction(o[name])); + } + }); + return result; + }; + + /** + * @ngdoc function + * @name ui.router.util.$urlMatcherFactory#type + * @methodOf ui.router.util.$urlMatcherFactory + * + * @description + * Registers a custom {@link ui.router.util.type:Type `Type`} object that can be used to + * generate URLs with typed parameters. + * + * @param {string} name The type name. + * @param {Object|Function} definition The type definition. See + * {@link ui.router.util.type:Type `Type`} for information on the values accepted. + * @param {Object|Function} definitionFn (optional) A function that is injected before the app + * runtime starts. The result of this function is merged into the existing `definition`. + * See {@link ui.router.util.type:Type `Type`} for information on the values accepted. + * + * @returns {Object} Returns `$urlMatcherFactoryProvider`. + * + * @example + * This is a simple example of a custom type that encodes and decodes items from an + * array, using the array index as the URL-encoded value: + * + *
      +   * var list = ['John', 'Paul', 'George', 'Ringo'];
      +   *
      +   * $urlMatcherFactoryProvider.type('listItem', {
      +   *   encode: function(item) {
      +   *     // Represent the list item in the URL using its corresponding index
      +   *     return list.indexOf(item);
      +   *   },
      +   *   decode: function(item) {
      +   *     // Look up the list item by index
      +   *     return list[parseInt(item, 10)];
      +   *   },
      +   *   is: function(item) {
      +   *     // Ensure the item is valid by checking to see that it appears
      +   *     // in the list
      +   *     return list.indexOf(item) > -1;
      +   *   }
      +   * });
      +   *
      +   * $stateProvider.state('list', {
      +   *   url: "/list/{item:listItem}",
      +   *   controller: function($scope, $stateParams) {
      +   *     console.log($stateParams.item);
      +   *   }
      +   * });
      +   *
      +   * // ...
      +   *
      +   * // Changes URL to '/list/3', logs "Ringo" to the console
      +   * $state.go('list', { item: "Ringo" });
      +   * 
      + * + * This is a more complex example of a type that relies on dependency injection to + * interact with services, and uses the parameter name from the URL to infer how to + * handle encoding and decoding parameter values: + * + *
      +   * // Defines a custom type that gets a value from a service,
      +   * // where each service gets different types of values from
      +   * // a backend API:
      +   * $urlMatcherFactoryProvider.type('dbObject', {}, function(Users, Posts) {
      +   *
      +   *   // Matches up services to URL parameter names
      +   *   var services = {
      +   *     user: Users,
      +   *     post: Posts
      +   *   };
      +   *
      +   *   return {
      +   *     encode: function(object) {
      +   *       // Represent the object in the URL using its unique ID
      +   *       return object.id;
      +   *     },
      +   *     decode: function(value, key) {
      +   *       // Look up the object by ID, using the parameter
      +   *       // name (key) to call the correct service
      +   *       return services[key].findById(value);
      +   *     },
      +   *     is: function(object, key) {
      +   *       // Check that object is a valid dbObject
      +   *       return angular.isObject(object) && object.id && services[key];
      +   *     }
      +   *     equals: function(a, b) {
      +   *       // Check the equality of decoded objects by comparing
      +   *       // their unique IDs
      +   *       return a.id === b.id;
      +   *     }
      +   *   };
      +   * });
      +   *
      +   * // In a config() block, you can then attach URLs with
      +   * // type-annotated parameters:
      +   * $stateProvider.state('users', {
      +   *   url: "/users",
      +   *   // ...
      +   * }).state('users.item', {
      +   *   url: "/{user:dbObject}",
      +   *   controller: function($scope, $stateParams) {
      +   *     // $stateParams.user will now be an object returned from
      +   *     // the Users service
      +   *   },
      +   *   // ...
      +   * });
      +   * 
      + */ + this.type = function (name, definition, definitionFn) { + if (!isDefined(definition)) return $types[name]; + if ($types.hasOwnProperty(name)) throw new Error("A type named '" + name + "' has already been defined."); + + $types[name] = new Type(extend({ name: name }, definition)); + if (definitionFn) { + typeQueue.push({ name: name, def: definitionFn }); + if (!enqueue) flushTypeQueue(); + } + return this; + }; + + // `flushTypeQueue()` waits until `$urlMatcherFactory` is injected before invoking the queued `definitionFn`s + function flushTypeQueue() { + while(typeQueue.length) { + var type = typeQueue.shift(); + if (type.pattern) throw new Error("You cannot override a type's .pattern at runtime."); + angular.extend($types[type.name], injector.invoke(type.def)); + } + } + + // Register default types. Store them in the prototype of $types. + forEach(defaultTypes, function(type, name) { $types[name] = new Type(extend({name: name}, type)); }); + $types = inherit($types, {}); + + /* No need to document $get, since it returns this */ + this.$get = ['$injector', function ($injector) { + injector = $injector; + enqueue = false; + flushTypeQueue(); + + forEach(defaultTypes, function(type, name) { + if (!$types[name]) $types[name] = new Type(type); + }); + return this; + }]; + + this.Param = function Param(id, type, config, location) { + var self = this; + config = unwrapShorthand(config); + type = getType(config, type, location); + var arrayMode = getArrayMode(); + type = arrayMode ? type.$asArray(arrayMode, location === "search") : type; + if (type.name === "string" && !arrayMode && location === "path" && config.value === undefined) + config.value = ""; // for 0.2.x; in 0.3.0+ do not automatically default to "" + var isOptional = config.value !== undefined; + var squash = getSquashPolicy(config, isOptional); + var replace = getReplace(config, arrayMode, isOptional, squash); + + function unwrapShorthand(config) { + var keys = isObject(config) ? objectKeys(config) : []; + var isShorthand = indexOf(keys, "value") === -1 && indexOf(keys, "type") === -1 && + indexOf(keys, "squash") === -1 && indexOf(keys, "array") === -1; + if (isShorthand) config = { value: config }; + config.$$fn = isInjectable(config.value) ? config.value : function () { return config.value; }; + return config; + } + + function getType(config, urlType, location) { + if (config.type && urlType) throw new Error("Param '"+id+"' has two type configurations."); + if (urlType) return urlType; + if (!config.type) return (location === "config" ? $types.any : $types.string); + + if (angular.isString(config.type)) + return $types[config.type]; + if (config.type instanceof Type) + return config.type; + return new Type(config.type); + } + + // array config: param name (param[]) overrides default settings. explicit config overrides param name. + function getArrayMode() { + var arrayDefaults = { array: (location === "search" ? "auto" : false) }; + var arrayParamNomenclature = id.match(/\[\]$/) ? { array: true } : {}; + return extend(arrayDefaults, arrayParamNomenclature, config).array; + } + + /** + * returns false, true, or the squash value to indicate the "default parameter url squash policy". + */ + function getSquashPolicy(config, isOptional) { + var squash = config.squash; + if (!isOptional || squash === false) return false; + if (!isDefined(squash) || squash == null) return defaultSquashPolicy; + if (squash === true || isString(squash)) return squash; + throw new Error("Invalid squash policy: '" + squash + "'. Valid policies: false, true, or arbitrary string"); + } + + function getReplace(config, arrayMode, isOptional, squash) { + var replace, configuredKeys, defaultPolicy = [ + { from: "", to: (isOptional || arrayMode ? undefined : "") }, + { from: null, to: (isOptional || arrayMode ? undefined : "") } + ]; + replace = isArray(config.replace) ? config.replace : []; + if (isString(squash)) + replace.push({ from: squash, to: undefined }); + configuredKeys = map(replace, function(item) { return item.from; } ); + return filter(defaultPolicy, function(item) { return indexOf(configuredKeys, item.from) === -1; }).concat(replace); + } + + /** + * [Internal] Get the default value of a parameter, which may be an injectable function. + */ + function $$getDefaultValue() { + if (!injector) throw new Error("Injectable functions cannot be called at configuration time"); + var defaultValue = injector.invoke(config.$$fn); + if (defaultValue !== null && defaultValue !== undefined && !self.type.is(defaultValue)) + throw new Error("Default value (" + defaultValue + ") for parameter '" + self.id + "' is not an instance of Type (" + self.type.name + ")"); + return defaultValue; + } + + /** + * [Internal] Gets the decoded representation of a value if the value is defined, otherwise, returns the + * default value, which may be the result of an injectable function. + */ + function $value(value) { + function hasReplaceVal(val) { return function(obj) { return obj.from === val; }; } + function $replace(value) { + var replacement = map(filter(self.replace, hasReplaceVal(value)), function(obj) { return obj.to; }); + return replacement.length ? replacement[0] : value; + } + value = $replace(value); + return !isDefined(value) ? $$getDefaultValue() : self.type.$normalize(value); + } + + function toString() { return "{Param:" + id + " " + type + " squash: '" + squash + "' optional: " + isOptional + "}"; } + + extend(this, { + id: id, + type: type, + location: location, + array: arrayMode, + squash: squash, + replace: replace, + isOptional: isOptional, + value: $value, + dynamic: undefined, + config: config, + toString: toString + }); + }; + + function ParamSet(params) { + extend(this, params || {}); + } + + ParamSet.prototype = { + $$new: function() { + return inherit(this, extend(new ParamSet(), { $$parent: this})); + }, + $$keys: function () { + var keys = [], chain = [], parent = this, + ignore = objectKeys(ParamSet.prototype); + while (parent) { chain.push(parent); parent = parent.$$parent; } + chain.reverse(); + forEach(chain, function(paramset) { + forEach(objectKeys(paramset), function(key) { + if (indexOf(keys, key) === -1 && indexOf(ignore, key) === -1) keys.push(key); + }); + }); + return keys; + }, + $$values: function(paramValues) { + var values = {}, self = this; + forEach(self.$$keys(), function(key) { + values[key] = self[key].value(paramValues && paramValues[key]); + }); + return values; + }, + $$equals: function(paramValues1, paramValues2) { + var equal = true, self = this; + forEach(self.$$keys(), function(key) { + var left = paramValues1 && paramValues1[key], right = paramValues2 && paramValues2[key]; + if (!self[key].type.equals(left, right)) equal = false; + }); + return equal; + }, + $$validates: function $$validate(paramValues) { + var keys = this.$$keys(), i, param, rawVal, normalized, encoded; + for (i = 0; i < keys.length; i++) { + param = this[keys[i]]; + rawVal = paramValues[keys[i]]; + if ((rawVal === undefined || rawVal === null) && param.isOptional) + break; // There was no parameter value, but the param is optional + normalized = param.type.$normalize(rawVal); + if (!param.type.is(normalized)) + return false; // The value was not of the correct Type, and could not be decoded to the correct Type + encoded = param.type.encode(normalized); + if (angular.isString(encoded) && !param.type.pattern.exec(encoded)) + return false; // The value was of the correct type, but when encoded, did not match the Type's regexp + } + return true; + }, + $$parent: undefined + }; + + this.ParamSet = ParamSet; +} + +// Register as a provider so it's available to other providers +angular.module('ui.router.util').provider('$urlMatcherFactory', $UrlMatcherFactory); +angular.module('ui.router.util').run(['$urlMatcherFactory', function($urlMatcherFactory) { }]); + +/** + * @ngdoc object + * @name ui.router.router.$urlRouterProvider + * + * @requires ui.router.util.$urlMatcherFactoryProvider + * @requires $locationProvider + * + * @description + * `$urlRouterProvider` has the responsibility of watching `$location`. + * When `$location` changes it runs through a list of rules one by one until a + * match is found. `$urlRouterProvider` is used behind the scenes anytime you specify + * a url in a state configuration. All urls are compiled into a UrlMatcher object. + * + * There are several methods on `$urlRouterProvider` that make it useful to use directly + * in your module config. + */ +$UrlRouterProvider.$inject = ['$locationProvider', '$urlMatcherFactoryProvider']; +function $UrlRouterProvider( $locationProvider, $urlMatcherFactory) { + var rules = [], otherwise = null, interceptDeferred = false, listener; + + // Returns a string that is a prefix of all strings matching the RegExp + function regExpPrefix(re) { + var prefix = /^\^((?:\\[^a-zA-Z0-9]|[^\\\[\]\^$*+?.()|{}]+)*)/.exec(re.source); + return (prefix != null) ? prefix[1].replace(/\\(.)/g, "$1") : ''; + } + + // Interpolates matched values into a String.replace()-style pattern + function interpolate(pattern, match) { + return pattern.replace(/\$(\$|\d{1,2})/, function (m, what) { + return match[what === '$' ? 0 : Number(what)]; + }); + } + + /** + * @ngdoc function + * @name ui.router.router.$urlRouterProvider#rule + * @methodOf ui.router.router.$urlRouterProvider + * + * @description + * Defines rules that are used by `$urlRouterProvider` to find matches for + * specific URLs. + * + * @example + *
      +   * 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 {function} rule Handler function that takes `$injector` and `$location` + * services as arguments. You can use them to return a valid path as a string. + * + * @return {object} `$urlRouterProvider` - `$urlRouterProvider` instance + */ + this.rule = function (rule) { + if (!isFunction(rule)) throw new Error("'rule' must be a function"); + rules.push(rule); + return this; + }; + + /** + * @ngdoc object + * @name ui.router.router.$urlRouterProvider#otherwise + * @methodOf ui.router.router.$urlRouterProvider + * + * @description + * Defines a path that is used when an invalid route is requested. + * + * @example + *
      +   * 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 {string|function} rule The url path you want to redirect to or a function + * rule that returns the url path. The function version is passed two params: + * `$injector` and `$location` services, and must return a url string. + * + * @return {object} `$urlRouterProvider` - `$urlRouterProvider` instance + */ + this.otherwise = function (rule) { + if (isString(rule)) { + var redirect = rule; + rule = function () { return redirect; }; + } + else if (!isFunction(rule)) throw new Error("'rule' must be a function"); + otherwise = rule; + return this; + }; + + + function handleIfMatch($injector, handler, match) { + if (!match) return false; + var result = $injector.invoke(handler, handler, { $match: match }); + return isDefined(result) ? result : true; + } + + /** + * @ngdoc function + * @name ui.router.router.$urlRouterProvider#when + * @methodOf ui.router.router.$urlRouterProvider + * + * @description + * 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 + *
      +   * 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 {string|object} what The incoming path that you want to redirect. + * @param {string|function} handler The path you want to redirect your user to. + */ + this.when = function (what, handler) { + var redirect, handlerIsString = isString(handler); + if (isString(what)) what = $urlMatcherFactory.compile(what); + + if (!handlerIsString && !isFunction(handler) && !isArray(handler)) + throw new Error("invalid 'handler' in when()"); + + var strategies = { + matcher: function (what, handler) { + if (handlerIsString) { + redirect = $urlMatcherFactory.compile(handler); + handler = ['$match', function ($match) { return redirect.format($match); }]; + } + return extend(function ($injector, $location) { + return handleIfMatch($injector, handler, what.exec($location.path(), $location.search())); + }, { + prefix: isString(what.prefix) ? what.prefix : '' + }); + }, + regex: function (what, handler) { + if (what.global || what.sticky) throw new Error("when() RegExp must not be global or sticky"); + + if (handlerIsString) { + redirect = handler; + handler = ['$match', function ($match) { return interpolate(redirect, $match); }]; + } + return extend(function ($injector, $location) { + return handleIfMatch($injector, handler, what.exec($location.path())); + }, { + prefix: regExpPrefix(what) + }); + } + }; + + var check = { matcher: $urlMatcherFactory.isMatcher(what), regex: what instanceof RegExp }; + + for (var n in check) { + if (check[n]) return this.rule(strategies[n](what, handler)); + } + + throw new Error("invalid 'what' in when()"); + }; + + /** + * @ngdoc function + * @name ui.router.router.$urlRouterProvider#deferIntercept + * @methodOf ui.router.router.$urlRouterProvider + * + * @description + * Disables (or enables) deferring location change interception. + * + * If you wish to customize the behavior of syncing the URL (for example, if you wish to + * defer a transition but maintain the current URL), call this method at configuration time. + * Then, at run time, call `$urlRouter.listen()` after you have configured your own + * `$locationChangeSuccess` event handler. + * + * @example + *
      +   * var app = angular.module('app', ['ui.router.router']);
      +   *
      +   * app.config(function ($urlRouterProvider) {
      +   *
      +   *   // Prevent $urlRouter from automatically intercepting URL changes;
      +   *   // this allows you to configure custom behavior in between
      +   *   // location changes and route synchronization:
      +   *   $urlRouterProvider.deferIntercept();
      +   *
      +   * }).run(function ($rootScope, $urlRouter, UserService) {
      +   *
      +   *   $rootScope.$on('$locationChangeSuccess', function(e) {
      +   *     // UserService is an example service for managing user state
      +   *     if (UserService.isLoggedIn()) return;
      +   *
      +   *     // Prevent $urlRouter's default handler from firing
      +   *     e.preventDefault();
      +   *
      +   *     UserService.handleLogin().then(function() {
      +   *       // Once the user has logged in, sync the current URL
      +   *       // to the router:
      +   *       $urlRouter.sync();
      +   *     });
      +   *   });
      +   *
      +   *   // Configures $urlRouter's listener *after* your custom listener
      +   *   $urlRouter.listen();
      +   * });
      +   * 
      + * + * @param {boolean} defer Indicates whether to defer location change interception. Passing + no parameter is equivalent to `true`. + */ + this.deferIntercept = function (defer) { + if (defer === undefined) defer = true; + interceptDeferred = defer; + }; + + /** + * @ngdoc object + * @name ui.router.router.$urlRouter + * + * @requires $location + * @requires $rootScope + * @requires $injector + * @requires $browser + * + * @description + * + */ + this.$get = $get; + $get.$inject = ['$location', '$rootScope', '$injector', '$browser', '$sniffer']; + function $get( $location, $rootScope, $injector, $browser, $sniffer) { + + var baseHref = $browser.baseHref(), location = $location.url(), lastPushedUrl; + + function appendBasePath(url, isHtml5, absolute) { + if (baseHref === '/') return url; + if (isHtml5) return baseHref.slice(0, -1) + url; + if (absolute) return baseHref.slice(1) + url; + return url; + } + + // TODO: Optimize groups of rules with non-empty prefix into some sort of decision tree + function update(evt) { + if (evt && evt.defaultPrevented) return; + var ignoreUpdate = lastPushedUrl && $location.url() === lastPushedUrl; + lastPushedUrl = undefined; + // TODO: Re-implement this in 1.0 for https://github.com/angular-ui/ui-router/issues/1573 + //if (ignoreUpdate) return true; + + function check(rule) { + var handled = rule($injector, $location); + + if (!handled) return false; + if (isString(handled)) $location.replace().url(handled); + return true; + } + var n = rules.length, i; + + for (i = 0; i < n; i++) { + if (check(rules[i])) return; + } + // always check otherwise last to allow dynamic updates to the set of rules + if (otherwise) check(otherwise); + } + + function listen() { + listener = listener || $rootScope.$on('$locationChangeSuccess', update); + return listener; + } + + if (!interceptDeferred) listen(); + + return { + /** + * @ngdoc function + * @name ui.router.router.$urlRouter#sync + * @methodOf ui.router.router.$urlRouter + * + * @description + * Triggers an update; the same update that happens when the address bar url changes, aka `$locationChangeSuccess`. + * This method is useful when you need to use `preventDefault()` on the `$locationChangeSuccess` event, + * perform some custom logic (route protection, auth, config, redirection, etc) and then finally proceed + * with the transition by calling `$urlRouter.sync()`. + * + * @example + *
      +       * angular.module('app', ['ui.router'])
      +       *   .run(function($rootScope, $urlRouter) {
      +       *     $rootScope.$on('$locationChangeSuccess', function(evt) {
      +       *       // Halt state change from even starting
      +       *       evt.preventDefault();
      +       *       // Perform custom logic
      +       *       var meetsRequirement = ...
      +       *       // Continue with the update and state transition if logic allows
      +       *       if (meetsRequirement) $urlRouter.sync();
      +       *     });
      +       * });
      +       * 
      + */ + sync: function() { + update(); + }, + + listen: function() { + return listen(); + }, + + update: function(read) { + if (read) { + location = $location.url(); + return; + } + if ($location.url() === location) return; + + $location.url(location); + $location.replace(); + }, + + push: function(urlMatcher, params, options) { + var url = urlMatcher.format(params || {}); + + // Handle the special hash param, if needed + if (url !== null && params && params['#']) { + url += '#' + params['#']; + } + + $location.url(url); + lastPushedUrl = options && options.$$avoidResync ? $location.url() : undefined; + if (options && options.replace) $location.replace(); + }, + + /** + * @ngdoc function + * @name ui.router.router.$urlRouter#href + * @methodOf ui.router.router.$urlRouter + * + * @description + * A URL generation method that returns the compiled URL for a given + * {@link ui.router.util.type:UrlMatcher `UrlMatcher`}, populated with the provided parameters. + * + * @example + *
      +       * $bob = $urlRouter.href(new UrlMatcher("/about/:person"), {
      +       *   person: "bob"
      +       * });
      +       * // $bob == "/about/bob";
      +       * 
      + * + * @param {UrlMatcher} urlMatcher The `UrlMatcher` object which is used as the template of the URL to generate. + * @param {object=} params An object of parameter values to fill the matcher's required parameters. + * @param {object=} options Options object. The options are: + * + * - **`absolute`** - {boolean=false}, If true will generate an absolute url, e.g. "http://www.example.com/fullurl". + * + * @returns {string} Returns the fully compiled URL, or `null` if `params` fail validation against `urlMatcher` + */ + href: function(urlMatcher, params, options) { + if (!urlMatcher.validates(params)) return null; + + var isHtml5 = $locationProvider.html5Mode(); + if (angular.isObject(isHtml5)) { + isHtml5 = isHtml5.enabled; + } + + isHtml5 = isHtml5 && $sniffer.history; + + var url = urlMatcher.format(params); + options = options || {}; + + if (!isHtml5 && url !== null) { + url = "#" + $locationProvider.hashPrefix() + url; + } + + // Handle special hash param, if needed + if (url !== null && params && params['#']) { + url += '#' + params['#']; + } + + url = appendBasePath(url, isHtml5, options.absolute); + + if (!options.absolute || !url) { + return url; + } + + var slash = (!isHtml5 && url ? '/' : ''), port = $location.port(); + port = (port === 80 || port === 443 ? '' : ':' + port); + + return [$location.protocol(), '://', $location.host(), port, slash, url].join(''); + } + }; + } +} + +angular.module('ui.router.router').provider('$urlRouter', $UrlRouterProvider); + +/** + * @ngdoc object + * @name ui.router.state.$stateProvider + * + * @requires ui.router.router.$urlRouterProvider + * @requires ui.router.util.$urlMatcherFactoryProvider + * + * @description + * The new `$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. + */ +$StateProvider.$inject = ['$urlRouterProvider', '$urlMatcherFactoryProvider']; +function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { + + var root, states = {}, $state, queue = {}, abstractKey = 'abstract'; + + // 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 = inherit(state.parent.data, state.data); + } + return state.data; + }, + + // Build a URLMatcher if necessary, either via a relative or absolute URL + url: function(state) { + var url = state.url, config = { params: state.params || {} }; + + if (isString(url)) { + if (url.charAt(0) == '^') return $urlMatcherFactory.compile(url.substring(1), config); + return (state.parent.navigable || root).url.concat(url, config); + } + + if (!url || $urlMatcherFactory.isMatcher(url)) 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); + }, + + // Own parameters for this state. state.url.params is already built at this point. Create and add non-url params + ownParams: function(state) { + var params = state.url && state.url.params || new $$UMFP.ParamSet(); + forEach(state.params || {}, function(config, id) { + if (!params[id]) params[id] = new $$UMFP.Param(id, null, config, "config"); + }); + return params; + }, + + // Derive parameters for this state and ensure they're a super-set of parent's parameters + params: function(state) { + var ownParams = pick(state.ownParams, state.ownParams.$$keys()); + return state.parent && state.parent.params ? extend(state.parent.params.$$new(), ownParams) : new $$UMFP.ParamSet(); + }, + + // 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; + view.resolveAs = view.resolveAs || state.resolveAs || '$resolve'; + views[name] = view; + }); + return views; + }, + + // 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: {} + }; + + function isRelative(stateName) { + return stateName.indexOf(".") === 0 || stateName.indexOf("^") === 0; + } + + function findState(stateOrName, base) { + if (!stateOrName) return undefined; + + 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 + "'"); + base = findState(base); + + var rel = name.split("."), i = 0, pathLength = rel.length, current = base; + + for (; i < pathLength; i++) { + if (rel[i] === "" && i === 0) { + current = base; + continue; + } + if (rel[i] === "^") { + if (!current.parent) throw new Error("Path '" + name + "' not valid for state '" + base.name + "'"); + current = current.parent; + continue; + } + break; + } + rel = rel.slice(i).join("."); + name = current.name + (current.name && rel ? "." : "") + rel; + } + var state = states[name]; + + if (state && (isStr || (!isStr && (state === stateOrName || state.self === stateOrName)))) { + return state; + } + return undefined; + } + + function queueState(parentName, state) { + if (!queue[parentName]) { + queue[parentName] = []; + } + queue[parentName].push(state); + } + + function flushQueuedChildren(parentName) { + var queued = queue[parentName] || []; + while(queued.length) { + registerState(queued.shift()); + } + } + + 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; } + }); + + 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 + : (isObject(state.parent) && isString(state.parent.name)) ? state.parent.name + : ''; + + // 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, { inherit: true, location: false }); + } + }]); + } + + // Register any queued children + flushQueuedChildren(name); + + return state; + } + + // Checks text to see if it looks like a glob. + function isGlob (text) { + return text.indexOf('*') > -1; + } + + // Returns true if glob matches current $state name. + function doesStateMatchGlob (glob) { + var globSegments = glob.split('.'), + segments = $state.$current.name.split('.'); + + //match single stars + for (var i = 0, l = globSegments.length; i < l; i++) { + if (globSegments[i] === '*') { + segments[i] = '*'; + } + } + + //match greedy starts + if (globSegments[0] === '**') { + segments = segments.slice(indexOf(segments, globSegments[1])); + segments.unshift('**'); + } + //match greedy ends + if (globSegments[globSegments.length - 1] === '**') { + segments.splice(indexOf(segments, globSegments[globSegments.length - 2]) + 1, Number.MAX_VALUE); + segments.push('**'); + } + + if (globSegments.length != segments.length) { + return false; + } + + return segments.join('') === globSegments.join(''); + } + + + // Implicit root state that is always active + root = registerState({ + name: '', + url: '^', + views: null, + 'abstract': true + }); + root.navigable = null; + + + /** + * @ngdoc function + * @name ui.router.state.$stateProvider#decorator + * @methodOf ui.router.state.$stateProvider + * + * @description + * Allows you to extend (carefully) or override (at your own peril) the + * `stateBuilder` object used internally by `$stateProvider`. 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:
      +   * $stateProvider.decorator('views', function (state, parent) {
      +   *   var result = {},
      +   *       views = parent(state);
      +   *
      +   *   angular.forEach(views, function (config, name) {
      +   *     var 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' }
      +   *   }
      +   * });
      +   *
      +   * // ...
      +   *
      +   * $state.go('home');
      +   * // Auto-populates list and item views with /partials/home/contact/list.html,
      +   * // and /partials/home/contact/item.html, respectively.
      +   * 
      + * + * @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 + */ + 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; + } + + /** + * @ngdoc function + * @name ui.router.state.$stateProvider#state + * @methodOf ui.router.state.$stateProvider + * + * @description + * Registers a state configuration under a given state name. The stateConfig object + * has the following acceptable properties. + * + * @param {string} name A unique state name, e.g. "home", "about", "contacts". + * To create a parent/child state use a dot, e.g. "about.sales", "home.newest". + * @param {object} stateConfig State configuration object. + * @param {string|function=} stateConfig.template + * + * html template as a string or a function that returns + * an html template as a string which should be used by the uiView directives. This property + * takes precedence over templateUrl. + * + * If `template` is a function, it will be called with the following parameters: + * + * - {array.<object>} - state parameters extracted from the current $location.path() by + * applying the current state + * + *
      template:
      +   *   "

      inline template definition

      " + + * "
      "
      + *
      template: function(params) {
      +   *       return "

      generated template

      "; }
      + *
      + * + * @param {string|function=} stateConfig.templateUrl + * + * + * path or function that returns a path to an html + * template that should be used by uiView. + * + * If `templateUrl` is a function, it will be called with the following parameters: + * + * - {array.<object>} - state parameters extracted from the current $location.path() by + * applying the current state + * + *
      templateUrl: "home.html"
      + *
      templateUrl: function(params) {
      +   *     return myTemplates[params.pageId]; }
      + * + * @param {function=} stateConfig.templateProvider + * + * Provider function that returns HTML content string. + *
       templateProvider:
      +   *       function(MyTemplateService, params) {
      +   *         return MyTemplateService.getTemplate(params.pageId);
      +   *       }
      + * + * @param {string|function=} stateConfig.controller + * + * + * Controller fn that should be associated with newly + * related scope or the name of a registered controller if passed as a string. + * Optionally, the ControllerAs may be declared here. + *
      controller: "MyRegisteredController"
      + *
      controller:
      +   *     "MyRegisteredController as fooCtrl"}
      + *
      controller: function($scope, MyService) {
      +   *     $scope.data = MyService.getData(); }
      + * + * @param {function=} stateConfig.controllerProvider + * + * + * Injectable provider function that returns the actual controller or string. + *
      controllerProvider:
      +   *   function(MyResolveData) {
      +   *     if (MyResolveData.foo)
      +   *       return "FooCtrl"
      +   *     else if (MyResolveData.bar)
      +   *       return "BarCtrl";
      +   *     else return function($scope) {
      +   *       $scope.baz = "Qux";
      +   *     }
      +   *   }
      + * + * @param {string=} stateConfig.controllerAs + * + * + * A controller alias name. If present the controller will be + * published to scope under the controllerAs name. + *
      controllerAs: "myCtrl"
      + * + * @param {string|object=} stateConfig.parent + * + * Optionally specifies the parent state of this state. + * + *
      parent: 'parentState'
      + *
      parent: parentState // JS variable
      + * + * @param {object=} stateConfig.resolve + * + * + * An optional map<string, function> of dependencies which + * should be injected into the controller. If any of these dependencies are promises, + * the router will wait for them all to be resolved before the controller is instantiated. + * If all the promises are resolved successfully, the $stateChangeSuccess event is fired + * and the values of the resolved promises are injected into any controllers that reference them. + * If any of the promises are rejected the $stateChangeError event is fired. + * + * The map object is: + * + * - key - {string}: name of dependency to be injected into controller + * - factory - {string|function}: If string then it is alias for service. Otherwise if function, + * it is injected and return value it treated as dependency. If result is a promise, it is + * resolved before its value is injected into controller. + * + *
      resolve: {
      +   *     myResolve1:
      +   *       function($http, $stateParams) {
      +   *         return $http.get("/api/foos/"+stateParams.fooID);
      +   *       }
      +   *     }
      + * + * @param {string=} stateConfig.url + * + * + * A url fragment with optional parameters. When a state is navigated or + * transitioned to, the `$stateParams` service will be populated with any + * parameters that were passed. + * + * (See {@link ui.router.util.type:UrlMatcher UrlMatcher} `UrlMatcher`} for + * more details on acceptable patterns ) + * + * examples: + *
      url: "/home"
      +   * url: "/users/:userid"
      +   * url: "/books/{bookid:[a-zA-Z_-]}"
      +   * url: "/books/{categoryid:int}"
      +   * url: "/books/{publishername:string}/{categoryid:int}"
      +   * url: "/messages?before&after"
      +   * url: "/messages?{before:date}&{after:date}"
      +   * url: "/messages/:mailboxid?{before:date}&{after:date}"
      +   * 
      + * + * @param {object=} stateConfig.views + * + * an optional map<string, object> which defined multiple views, or targets views + * manually/explicitly. + * + * Examples: + * + * Targets three named `ui-view`s in the parent state's template + *
      views: {
      +   *     header: {
      +   *       controller: "headerCtrl",
      +   *       templateUrl: "header.html"
      +   *     }, body: {
      +   *       controller: "bodyCtrl",
      +   *       templateUrl: "body.html"
      +   *     }, footer: {
      +   *       controller: "footCtrl",
      +   *       templateUrl: "footer.html"
      +   *     }
      +   *   }
      + * + * Targets named `ui-view="header"` from grandparent state 'top''s template, and named `ui-view="body" from parent state's template. + *
      views: {
      +   *     'header@top': {
      +   *       controller: "msgHeaderCtrl",
      +   *       templateUrl: "msgHeader.html"
      +   *     }, 'body': {
      +   *       controller: "messagesCtrl",
      +   *       templateUrl: "messages.html"
      +   *     }
      +   *   }
      + * + * @param {boolean=} [stateConfig.abstract=false] + * + * An abstract state will never be directly activated, + * but can provide inherited properties to its common children states. + *
      abstract: true
      + * + * @param {function=} stateConfig.onEnter + * + * + * Callback function for when a state is entered. Good way + * to trigger an action or dispatch an event, such as opening a dialog. + * If minifying your scripts, make sure to explicitly annotate this function, + * because it won't be automatically annotated by your build tools. + * + *
      onEnter: function(MyService, $stateParams) {
      +   *     MyService.foo($stateParams.myParam);
      +   * }
      + * + * @param {function=} stateConfig.onExit + * + * + * Callback function for when a state is exited. Good way to + * trigger an action or dispatch an event, such as opening a dialog. + * If minifying your scripts, make sure to explicitly annotate this function, + * because it won't be automatically annotated by your build tools. + * + *
      onExit: function(MyService, $stateParams) {
      +   *     MyService.cleanup($stateParams.myParam);
      +   * }
      + * + * @param {boolean=} [stateConfig.reloadOnSearch=true] + * + * + * If `false`, will not retrigger the same state + * just because a search/query parameter has changed (via $location.search() or $location.hash()). + * Useful for when you'd like to modify $location.search() without triggering a reload. + *
      reloadOnSearch: false
      + * + * @param {object=} stateConfig.data + * + * + * Arbitrary data object, useful for custom configuration. The parent state's `data` is + * prototypally inherited. In other words, adding a data property to a state adds it to + * the entire subtree via prototypal inheritance. + * + *
      data: {
      +   *     requiredRole: 'foo'
      +   * } 
      + * + * @param {object=} stateConfig.params + * + * + * A map which optionally configures parameters declared in the `url`, or + * defines additional non-url parameters. For each parameter being + * configured, add a configuration object keyed to the name of the parameter. + * + * Each parameter configuration object may contain the following properties: + * + * - ** value ** - {object|function=}: specifies the default value for this + * parameter. This implicitly sets this parameter as optional. + * + * When UI-Router routes to a state and no value is + * specified for this parameter in the URL or transition, the + * default value will be used instead. If `value` is a function, + * it will be injected and invoked, and the return value used. + * + * *Note*: `undefined` is treated as "no default value" while `null` + * is treated as "the default value is `null`". + * + * *Shorthand*: If you only need to configure the default value of the + * parameter, you may use a shorthand syntax. In the **`params`** + * map, instead mapping the param name to a full parameter configuration + * object, simply set map it to the default parameter value, e.g.: + * + *
      // define a parameter's default value
      +   * params: {
      +   *     param1: { value: "defaultValue" }
      +   * }
      +   * // shorthand default values
      +   * params: {
      +   *     param1: "defaultValue",
      +   *     param2: "param2Default"
      +   * }
      + * + * - ** array ** - {boolean=}: *(default: false)* If true, the param value will be + * treated as an array of values. If you specified a Type, the value will be + * treated as an array of the specified Type. Note: query parameter values + * default to a special `"auto"` mode. + * + * For query parameters in `"auto"` mode, if multiple values for a single parameter + * are present in the URL (e.g.: `/foo?bar=1&bar=2&bar=3`) then the values + * are mapped to an array (e.g.: `{ foo: [ '1', '2', '3' ] }`). However, if + * only one value is present (e.g.: `/foo?bar=1`) then the value is treated as single + * value (e.g.: `{ foo: '1' }`). + * + *
      params: {
      +   *     param1: { array: true }
      +   * }
      + * + * - ** squash ** - {bool|string=}: `squash` configures how a default parameter value is represented in the URL when + * the current parameter value is the same as the default value. If `squash` is not set, it uses the + * configured default squash policy. + * (See {@link ui.router.util.$urlMatcherFactory#methods_defaultSquashPolicy `defaultSquashPolicy()`}) + * + * There are three squash settings: + * + * - false: The parameter's default value is not squashed. It is encoded and included in the URL + * - true: The parameter's default value is omitted from the URL. If the parameter is preceeded and followed + * by slashes in the state's `url` declaration, then one of those slashes are omitted. + * This can allow for cleaner looking URLs. + * - `""`: The parameter's default value is replaced with an arbitrary placeholder of your choice. + * + *
      params: {
      +   *     param1: {
      +   *       value: "defaultId",
      +   *       squash: true
      +   * } }
      +   * // squash "defaultValue" to "~"
      +   * params: {
      +   *     param1: {
      +   *       value: "defaultValue",
      +   *       squash: "~"
      +   * } }
      +   * 
      + * + * + * @example + *
      +   * // Some state name examples
      +   *
      +   * // stateName can be a single top-level name (must be unique).
      +   * $stateProvider.state("home", {});
      +   *
      +   * // Or it can be a nested state name. This state is a child of the
      +   * // above "home" state.
      +   * $stateProvider.state("home.newest", {});
      +   *
      +   * // Nest states as deeply as needed.
      +   * $stateProvider.state("home.newest.abc.xyz.inception", {});
      +   *
      +   * // state() returns $stateProvider, so you can chain state declarations.
      +   * $stateProvider
      +   *   .state("home", {})
      +   *   .state("about", {})
      +   *   .state("contacts", {});
      +   * 
      + * + */ + this.state = state; + function state(name, definition) { + /*jshint validthis: true */ + if (isObject(name)) definition = name; + else definition.name = name; + registerState(definition); + return this; + } + + /** + * @ngdoc object + * @name ui.router.state.$state + * + * @requires $rootScope + * @requires $q + * @requires ui.router.state.$view + * @requires $injector + * @requires ui.router.util.$resolve + * @requires ui.router.state.$stateParams + * @requires ui.router.router.$urlRouter + * + * @property {object} params A param object, e.g. {sectionId: section.id)}, that + * you'd like to test against the current active state. + * @property {object} current A reference to the state's config object. However + * you passed it in. Useful for accessing custom data. + * @property {object} transition Currently pending transition. A promise that'll + * resolve or reject. + * + * @description + * `$state` service is responsible for representing states as well as transitioning + * between them. It also provides interfaces to ask for current state or even states + * you're coming from. + */ + this.$get = $get; + $get.$inject = ['$rootScope', '$q', '$view', '$injector', '$resolve', '$stateParams', '$urlRouter', '$location', '$urlMatcherFactory']; + function $get( $rootScope, $q, $view, $injector, $resolve, $stateParams, $urlRouter, $location, $urlMatcherFactory) { + + 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')); + + // Handles the case where a state which is the target of a transition is not found, and the user + // can optionally retry or defer the transition + function handleRedirect(redirect, state, params, options) { + /** + * @ngdoc event + * @name ui.router.state.$state#$stateNotFound + * @eventOf ui.router.state.$state + * @eventType broadcast on root scope + * @description + * Fired when a requested state **cannot be found** using the provided state name during transition. + * The event is broadcast allowing any handlers a single chance to deal with the error (usually by + * lazy-loading the unfound state). A special `unfoundState` object is passed to the listener handler, + * you can see its three properties in the example. You can use `event.preventDefault()` to abort the + * transition and the promise returned from `go` will be rejected with a `'transition aborted'` value. + * + * @param {Object} event Event object. + * @param {Object} unfoundState Unfound State information. Contains: `to, toParams, options` properties. + * @param {State} fromState Current state object. + * @param {Object} fromParams Current state params. + * + * @example + * + *
      +       * // somewhere, assume lazy.state has not been defined
      +       * $state.go("lazy.state", {a:1, b:2}, {inherit:false});
      +       *
      +       * // somewhere else
      +       * $scope.$on('$stateNotFound',
      +       * function(event, unfoundState, fromState, fromParams){
      +       *     console.log(unfoundState.to); // "lazy.state"
      +       *     console.log(unfoundState.toParams); // {a:1, b:2}
      +       *     console.log(unfoundState.options); // {inherit:false} + default options
      +       * })
      +       * 
      + */ + var evt = $rootScope.$broadcast('$stateNotFound', redirect, state, params); + + if (evt.defaultPrevented) { + $urlRouter.update(); + return TransitionAborted; + } + + if (!evt.retry) { + return null; + } + + // Allow the handler to return a promise to defer state lookup retry + if (options.$retry) { + $urlRouter.update(); + 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; + }); + $urlRouter.update(); + + return retryTransition; + } + + root.locals = { resolve: null, globals: { $stateParams: {} } }; + + $state = { + params: {}, + current: root.self, + $current: root, + transition: null + }; + + /** + * @ngdoc function + * @name ui.router.state.$state#reload + * @methodOf ui.router.state.$state + * + * @description + * A method that force reloads the current state. All resolves are re-resolved, + * controllers reinstantiated, and events re-fired. + * + * @example + *
      +     * var app angular.module('app', ['ui.router']);
      +     *
      +     * app.controller('ctrl', function ($scope, $state) {
      +     *   $scope.reload = function(){
      +     *     $state.reload();
      +     *   }
      +     * });
      +     * 
      + * + * `reload()` is just an alias for: + *
      +     * $state.transitionTo($state.current, $stateParams, { 
      +     *   reload: true, inherit: false, notify: true
      +     * });
      +     * 
      + * + * @param {string=|object=} state - A state name or a state object, which is the root of the resolves to be re-resolved. + * @example + *
      +     * //assuming app application consists of 3 states: 'contacts', 'contacts.detail', 'contacts.detail.item' 
      +     * //and current state is 'contacts.detail.item'
      +     * var app angular.module('app', ['ui.router']);
      +     *
      +     * app.controller('ctrl', function ($scope, $state) {
      +     *   $scope.reload = function(){
      +     *     //will reload 'contact.detail' and 'contact.detail.item' states
      +     *     $state.reload('contact.detail');
      +     *   }
      +     * });
      +     * 
      + * + * `reload()` is just an alias for: + *
      +     * $state.transitionTo($state.current, $stateParams, { 
      +     *   reload: true, inherit: false, notify: true
      +     * });
      +     * 
      + + * @returns {promise} A promise representing the state of the new transition. See + * {@link ui.router.state.$state#methods_go $state.go}. + */ + $state.reload = function reload(state) { + return $state.transitionTo($state.current, $stateParams, { reload: state || true, inherit: false, notify: true}); + }; + + /** + * @ngdoc function + * @name ui.router.state.$state#go + * @methodOf ui.router.state.$state + * + * @description + * Convenience method for transitioning to a new state. `$state.go` calls + * `$state.transitionTo` internally but automatically sets options to + * `{ location: true, inherit: true, relative: $state.$current, notify: true }`. + * This allows you to easily use an absolute or relative to path and specify + * only the parameters you'd like to update (while letting unspecified parameters + * inherit from the currently active ancestor states). + * + * @example + *
      +     * var app = angular.module('app', ['ui.router']);
      +     *
      +     * app.controller('ctrl', function ($scope, $state) {
      +     *   $scope.changeState = function () {
      +     *     $state.go('contact.detail');
      +     *   };
      +     * });
      +     * 
      + * + * + * @param {string} to Absolute state name or relative state path. Some examples: + * + * - `$state.go('contact.detail')` - will go to the `contact.detail` state + * - `$state.go('^')` - will go to a parent state + * - `$state.go('^.sibling')` - will go to a sibling state + * - `$state.go('.child.grandchild')` - will go to grandchild state + * + * @param {object=} 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 currently + * defined parameters. Only parameters specified in the state definition can be overridden, new + * parameters will be ignored. This allows, for example, going to a sibling state that shares parameters + * specified in a parent state. Parameter inheritance only works between common ancestor states, I.e. + * transitioning to a sibling will get you the parameters for all parents, transitioning to a child + * will get you all current parameters, etc. + * @param {object=} options Options object. The options are: + * + * - **`location`** - {boolean=true|string=} - If `true` will update the url in the location bar, if `false` + * will not. If string, must be `"replace"`, which will update url and also replace last history record. + * - **`inherit`** - {boolean=true}, If `true` will inherit url parameters from current url. + * - **`relative`** - {object=$state.$current}, When transitioning with relative path (e.g '^'), + * defines which state to be relative from. + * - **`notify`** - {boolean=true}, If `true` will broadcast $stateChangeStart and $stateChangeSuccess events. + * - **`reload`** (v0.2.5) - {boolean=false|string|object}, If `true` will force transition even if no state or params + * have changed. It will reload the resolves and views of the current state and parent states. + * If `reload` is a string (or state object), the state object is fetched (by name, or object reference); and \ + * the transition reloads the resolves and views for that matched state, and all its children states. + * + * @returns {promise} A promise representing the state of the new transition. + * + * Possible success values: + * + * - $state.current + * + *
      Possible rejection values: + * + * - 'transition superseded' - when a newer transition has been started after this one + * - 'transition prevented' - when `event.preventDefault()` has been called in a `$stateChangeStart` listener + * - 'transition aborted' - when `event.preventDefault()` has been called in a `$stateNotFound` listener or + * when a `$stateNotFound` `event.retry` promise errors. + * - 'transition failed' - when a state has been unsuccessfully found after 2 tries. + * - *resolve error* - when an error has occurred with a `resolve` + * + */ + $state.go = function go(to, params, options) { + return $state.transitionTo(to, params, extend({ inherit: true, relative: $state.$current }, options)); + }; + + /** + * @ngdoc function + * @name ui.router.state.$state#transitionTo + * @methodOf ui.router.state.$state + * + * @description + * Low-level method for transitioning to a new state. {@link ui.router.state.$state#methods_go $state.go} + * uses `transitionTo` internally. `$state.go` is recommended in most situations. + * + * @example + *
      +     * var app = angular.module('app', ['ui.router']);
      +     *
      +     * app.controller('ctrl', function ($scope, $state) {
      +     *   $scope.changeState = function () {
      +     *     $state.transitionTo('contact.detail');
      +     *   };
      +     * });
      +     * 
      + * + * @param {string} to State name. + * @param {object=} toParams A map of the parameters that will be sent to the state, + * will populate $stateParams. + * @param {object=} options Options object. The options are: + * + * - **`location`** - {boolean=true|string=} - If `true` will update the url in the location bar, if `false` + * will not. If string, must be `"replace"`, which will update url and also replace last history record. + * - **`inherit`** - {boolean=false}, If `true` will inherit url parameters from current url. + * - **`relative`** - {object=}, When transitioning with relative path (e.g '^'), + * defines which state to be relative from. + * - **`notify`** - {boolean=true}, If `true` will broadcast $stateChangeStart and $stateChangeSuccess events. + * - **`reload`** (v0.2.5) - {boolean=false|string=|object=}, If `true` will force transition even if the state or params + * have not changed, aka a reload of the same state. It differs from reloadOnSearch because you'd + * use this when you want to force a reload when *everything* is the same, including search params. + * if String, then will reload the state with the name given in reload, and any children. + * if Object, then a stateObj is expected, will reload the state found in stateObj, and any children. + * + * @returns {promise} A promise representing the state of the new transition. See + * {@link ui.router.state.$state#methods_go $state.go}. + */ + $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); + + // Store the hash param for later (since it will be stripped out by various methods) + var hash = toParams['#']; + + if (!isDefined(toState)) { + var redirect = { to: to, toParams: toParams, options: options }; + var redirectResult = handleRedirect(redirect, from.self, fromParams, options); + + if (redirectResult) { + return redirectResult; + } + + // 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("No such state '" + to + "'"); + throw new Error("Could not resolve '" + to + "' from state '" + options.relative + "'"); + } + } + if (toState[abstractKey]) throw new Error("Cannot transition to abstract state '" + to + "'"); + if (options.inherit) toParams = inheritParams($stateParams, toParams || {}, $state.$current, toState); + if (!toState.params.$$validates(toParams)) return TransitionFailed; + + toParams = toState.params.$$values(toParams); + to = toState; + + var toPath = to.path; + + // Starting from the root of the path, keep all levels that haven't changed + var keep = 0, state = toPath[keep], locals = root.locals, toLocals = []; + + if (!options.reload) { + while (state && state === fromPath[keep] && state.ownParams.$$equals(toParams, fromParams)) { + locals = toLocals[keep] = state.locals; + keep++; + state = toPath[keep]; + } + } else if (isString(options.reload) || isObject(options.reload)) { + if (isObject(options.reload) && !options.reload.name) { + throw new Error('Invalid reload state object'); + } + + var reloadState = options.reload === true ? fromPath[0] : findState(options.reload); + if (options.reload && !reloadState) { + throw new Error("No such reload state '" + (isString(options.reload) ? options.reload : options.reload.name) + "'"); + } + + while (state && state === fromPath[keep] && state !== reloadState) { + locals = toLocals[keep] = state.locals; + keep++; + state = toPath[keep]; + } + } + + // 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 (shouldSkipReload(to, toParams, from, fromParams, locals, options)) { + if (hash) toParams['#'] = hash; + $state.params = toParams; + copy($state.params, $stateParams); + copy(filterByKeys(to.params.$$keys(), $stateParams), to.locals.globals.$stateParams); + if (options.location && to.navigable && to.navigable.url) { + $urlRouter.push(to.navigable.url, toParams, { + $$avoidResync: true, replace: options.location === 'replace' + }); + $urlRouter.update(true); + } + $state.transition = null; + return $q.when($state.current); + } + + // Filter parameters before we pass them to event handlers etc. + toParams = filterByKeys(to.params.$$keys(), toParams || {}); + + // Re-add the saved hash before we start returning things or broadcasting $stateChangeStart + if (hash) toParams['#'] = hash; + + // Broadcast start event and cancel the transition if requested + if (options.notify) { + /** + * @ngdoc event + * @name ui.router.state.$state#$stateChangeStart + * @eventOf ui.router.state.$state + * @eventType broadcast on root scope + * @description + * Fired when the state transition **begins**. You can use `event.preventDefault()` + * to prevent the transition from happening and then the transition promise will be + * rejected with a `'transition prevented'` value. + * + * @param {Object} event Event object. + * @param {State} toState The state being transitioned to. + * @param {Object} toParams The params supplied to the `toState`. + * @param {State} fromState The current state, pre-transition. + * @param {Object} fromParams The params supplied to the `fromState`. + * + * @example + * + *
      +         * $rootScope.$on('$stateChangeStart',
      +         * function(event, toState, toParams, fromState, fromParams){
      +         *     event.preventDefault();
      +         *     // transitionTo() promise will be rejected with
      +         *     // a 'transition prevented' error
      +         * })
      +         * 
      + */ + if ($rootScope.$broadcast('$stateChangeStart', to.self, toParams, from.self, fromParams, options).defaultPrevented) { + $rootScope.$broadcast('$stateChangeCancel', to.self, toParams, from.self, fromParams); + //Don't update and resync url if there's been a new transition started. see issue #2238, #600 + if ($state.transition == null) $urlRouter.update(); + 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 < toPath.length; l++, state = toPath[l]) { + locals = toLocals[l] = inherit(locals); + resolved = resolveState(state, toParams, state === to, resolved, locals, options); + } + + // Once everything is resolved, we are ready to perform the actual transition + // and return a promise for the new state. We also keep track of what the + // current promise is, so that we can detect overlapping transitions and + // keep only the outcome of the last transition. + var transition = $state.transition = resolved.then(function () { + var l, entering, exiting; + + if ($state.transition !== transition) return TransitionSuperseded; + + // Exit 'from' states not kept + for (l = fromPath.length - 1; 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 < toPath.length; l++) { + entering = toPath[l]; + entering.locals = toLocals[l]; + if (entering.self.onEnter) { + $injector.invoke(entering.self.onEnter, entering.self, entering.locals.globals); + } + } + + // Run it again, to catch any transitions in callbacks + if ($state.transition !== transition) return TransitionSuperseded; + + // Update globals in $state + $state.$current = to; + $state.current = to.self; + $state.params = toParams; + copy($state.params, $stateParams); + $state.transition = null; + + if (options.location && to.navigable) { + $urlRouter.push(to.navigable.url, to.navigable.locals.globals.$stateParams, { + $$avoidResync: true, replace: options.location === 'replace' + }); + } + + if (options.notify) { + /** + * @ngdoc event + * @name ui.router.state.$state#$stateChangeSuccess + * @eventOf ui.router.state.$state + * @eventType broadcast on root scope + * @description + * Fired once the state transition is **complete**. + * + * @param {Object} event Event object. + * @param {State} toState The state being transitioned to. + * @param {Object} toParams The params supplied to the `toState`. + * @param {State} fromState The current state, pre-transition. + * @param {Object} fromParams The params supplied to the `fromState`. + */ + $rootScope.$broadcast('$stateChangeSuccess', to.self, toParams, from.self, fromParams); + } + $urlRouter.update(true); + + return $state.current; + }, function (error) { + if ($state.transition !== transition) return TransitionSuperseded; + + $state.transition = null; + /** + * @ngdoc event + * @name ui.router.state.$state#$stateChangeError + * @eventOf ui.router.state.$state + * @eventType broadcast on root scope + * @description + * Fired when an **error occurs** during transition. It's important to note that if you + * have any errors in your resolve functions (javascript errors, non-existent services, etc) + * they will not throw traditionally. You must listen for this $stateChangeError event to + * catch **ALL** errors. + * + * @param {Object} event Event object. + * @param {State} toState The state being transitioned to. + * @param {Object} toParams The params supplied to the `toState`. + * @param {State} fromState The current state, pre-transition. + * @param {Object} fromParams The params supplied to the `fromState`. + * @param {Error} error The resolve error object. + */ + evt = $rootScope.$broadcast('$stateChangeError', to.self, toParams, from.self, fromParams, error); + + if (!evt.defaultPrevented) { + $urlRouter.update(); + } + + return $q.reject(error); + }); + + return transition; + }; + + /** + * @ngdoc function + * @name ui.router.state.$state#is + * @methodOf ui.router.state.$state + * + * @description + * Similar to {@link ui.router.state.$state#methods_includes $state.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 + *
      +     * $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
      +     * 
      Item
      + *
      + * + * @param {string|object} stateOrName The state name (absolute or relative) or state object you'd like to check. + * @param {object=} params A param object, e.g. `{sectionId: section.id}`, that you'd like + * to test against the current active state. + * @param {object=} options An options object. The options are: + * + * - **`relative`** - {string|object} - 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 is the state. + */ + $state.is = function is(stateOrName, params, options) { + options = extend({ relative: $state.$current }, options || {}); + var state = findState(stateOrName, options.relative); + + if (!isDefined(state)) { return undefined; } + if ($state.$current !== state) { return false; } + return params ? equalForKeys(state.params.$$values(params), $stateParams) : true; + }; + + /** + * @ngdoc function + * @name ui.router.state.$state#includes + * @methodOf ui.router.state.$state + * + * @description + * 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 + * Partial and relative names + *
      +     * $state.$current.name = 'contacts.details.item';
      +     *
      +     * // 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
      +     *
      +     * // Using relative names (. and ^), typically from a template
      +     * // E.g. from the 'contacts.details' template
      +     * 
      Item
      + *
      + * + * Basic globbing patterns + *
      +     * $state.$current.name = 'contacts.details.item.url';
      +     *
      +     * $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 {string} stateOrName A partial name, relative name, or glob pattern + * to be searched for within the current state name. + * @param {object=} params A param object, e.g. `{sectionId: section.id}`, + * that you'd like to test against the current active state. + * @param {object=} options An options object. The options are: + * + * - **`relative`** - {string|object=} - If `stateOrName` is a relative state reference and `options.relative` is set, + * .includes will test relative to `options.relative` state (or name). + * + * @returns {boolean} Returns true if it does include the state + */ + $state.includes = function includes(stateOrName, params, options) { + options = extend({ relative: $state.$current }, options || {}); + if (isString(stateOrName) && isGlob(stateOrName)) { + if (!doesStateMatchGlob(stateOrName)) { + return false; + } + stateOrName = $state.$current.name; + } + + var state = findState(stateOrName, options.relative); + if (!isDefined(state)) { return undefined; } + if (!isDefined($state.$current.includes[state.name])) { return false; } + return params ? equalForKeys(state.params.$$values(params), $stateParams, objectKeys(params)) : true; + }; + + + /** + * @ngdoc function + * @name ui.router.state.$state#href + * @methodOf ui.router.state.$state + * + * @description + * A url generation method that returns the compiled url for the given state populated with the given params. + * + * @example + *
      +     * expect($state.href("about.person", { person: "bob" })).toEqual("/about/bob");
      +     * 
      + * + * @param {string|object} stateOrName The state name or state object you'd like to generate a url from. + * @param {object=} params An object of parameter values to fill the state's required parameters. + * @param {object=} options Options object. The options are: + * + * - **`lossy`** - {boolean=true} - If true, and if there is no url associated with the state provided in the + * first parameter, then the constructed href url will be built from the first navigable ancestor (aka + * ancestor with a valid url). + * - **`inherit`** - {boolean=true}, If `true` will inherit url parameters from current url. + * - **`relative`** - {object=$state.$current}, When transitioning with relative path (e.g '^'), + * defines which state to be relative from. + * - **`absolute`** - {boolean=false}, If true will generate an absolute url, e.g. "http://www.example.com/fullurl". + * + * @returns {string} compiled state url + */ + $state.href = function href(stateOrName, params, options) { + options = extend({ + lossy: true, + inherit: true, + absolute: false, + relative: $state.$current + }, options || {}); + + var state = findState(stateOrName, options.relative); + + if (!isDefined(state)) return null; + if (options.inherit) params = inheritParams($stateParams, params || {}, $state.$current, state); + + var nav = (state && options.lossy) ? state.navigable : state; + + if (!nav || nav.url === undefined || nav.url === null) { + return null; + } + return $urlRouter.href(nav.url, filterByKeys(state.params.$$keys().concat('#'), params || {}), { + absolute: options.absolute + }); + }; + + /** + * @ngdoc function + * @name ui.router.state.$state#get + * @methodOf ui.router.state.$state + * + * @description + * Returns the state configuration object for any specific state or all states. + * + * @param {string|object=} stateOrName (absolute or relative) If provided, will only get the config for + * the requested state. If not provided, returns an array of ALL state configs. + * @param {string|object=} context When stateOrName is a relative state reference, the state will be retrieved relative to context. + * @returns {Object|Array} State configuration object or array of all objects. + */ + $state.get = function (stateOrName, context) { + if (arguments.length === 0) return map(objectKeys(states), function(name) { return states[name].self; }); + var state = findState(stateOrName, context || $state.$current); + return (state && state.self) ? state.self : null; + }; + + function resolveState(state, params, paramsAreFiltered, inherited, dst, options) { + // Make a restricted $stateParams with only the parameters that apply to this state if + // necessary. In addition to being available to the controller and onEnter/onExit callbacks, + // we also need $stateParams to be available for any $injector calls we make during the + // dependency resolution process. + var $stateParams = (paramsAreFiltered) ? params : filterByKeys(state.params.$$keys(), params); + var locals = { $stateParams: $stateParams }; + + // Resolve 'global' dependencies for the state, i.e. those not specific to a view. + // We're also including $stateParams in this; that way the parameters are restricted + // to the set that should be visible to the state, and are independent of when we update + // the global $state and $stateParams values. + dst.resolve = $resolve.resolve(state.resolve, locals, dst.resolve, state); + var promises = [dst.resolve.then(function (globals) { + dst.globals = globals; + })]; + if (inherited) promises.push(inherited); + + function resolveViews() { + var viewsPromises = []; + + // Resolve template and dependencies for all views. + forEach(state.views, function (view, name) { + var injectables = (view.resolve && view.resolve !== state.resolve ? view.resolve : {}); + injectables.$template = [ function () { + return $view.load(name, { view: view, locals: dst.globals, params: $stateParams, notify: options.notify }) || ''; + }]; + + viewsPromises.push($resolve.resolve(injectables, dst.globals, dst.resolve, state).then(function (result) { + // References to the controller (only instantiated at link time) + if (isFunction(view.controllerProvider) || isArray(view.controllerProvider)) { + var injectLocals = angular.extend({}, injectables, dst.globals); + result.$$controller = $injector.invoke(view.controllerProvider, null, injectLocals); + } else { + result.$$controller = view.controller; + } + // Provide access to the state itself for internal use + result.$$state = state; + result.$$controllerAs = view.controllerAs; + result.$$resolveAs = view.resolveAs; + dst[name] = result; + })); + }); + + return $q.all(viewsPromises).then(function(){ + return dst.globals; + }); + } + + // Wait for all the promises and then return the activation object + return $q.all(promises).then(resolveViews).then(function (values) { + return dst; + }); + } + + return $state; + } + + function shouldSkipReload(to, toParams, from, fromParams, locals, options) { + // Return true if there are no differences in non-search (path/object) params, false if there are differences + function nonSearchParamsEqual(fromAndToState, fromParams, toParams) { + // Identify whether all the parameters that differ between `fromParams` and `toParams` were search params. + function notSearchParam(key) { + return fromAndToState.params[key].location != "search"; + } + var nonQueryParamKeys = fromAndToState.params.$$keys().filter(notSearchParam); + var nonQueryParams = pick.apply({}, [fromAndToState.params].concat(nonQueryParamKeys)); + var nonQueryParamSet = new $$UMFP.ParamSet(nonQueryParams); + return nonQueryParamSet.$$equals(fromParams, toParams); + } + + // If reload was not explicitly requested + // and we're transitioning to the same state we're already in + // and the locals didn't change + // or they changed in a way that doesn't merit reloading + // (reloadOnParams:false, or reloadOnSearch.false and only search params changed) + // Then return true. + if (!options.reload && to === from && + (locals === from.locals || (to.self.reloadOnSearch === false && nonSearchParamsEqual(from, fromParams, toParams)))) { + return true; + } + } +} + +angular.module('ui.router.state') + .factory('$stateParams', function () { return {}; }) + .constant("$state.runtime", { autoinject: true }) + .provider('$state', $StateProvider) + // Inject $state to initialize when entering runtime. #2574 + .run(['$injector', function ($injector) { + // Allow tests (stateSpec.js) to turn this off by defining this constant + if ($injector.get("$state.runtime").autoinject) { + $injector.get('$state'); + } + }]); + + +$ViewProvider.$inject = []; +function $ViewProvider() { + + this.$get = $get; + /** + * @ngdoc object + * @name ui.router.state.$view + * + * @requires ui.router.util.$templateFactory + * @requires $rootScope + * + * @description + * + */ + $get.$inject = ['$rootScope', '$templateFactory']; + function $get( $rootScope, $templateFactory) { + return { + // $view.load('full.viewName', { template: ..., controller: ..., resolve: ..., async: false, params: ... }) + /** + * @ngdoc function + * @name ui.router.state.$view#load + * @methodOf ui.router.state.$view + * + * @description + * + * @param {string} name name + * @param {object} options option object. + */ + load: function load(name, options) { + var result, defaults = { + template: null, controller: null, view: null, locals: null, notify: true, async: true, params: {} + }; + options = extend(defaults, options); + + if (options.view) { + result = $templateFactory.fromConfig(options.view, options.params, options.locals); + } + return result; + } + }; + } +} + +angular.module('ui.router.state').provider('$view', $ViewProvider); + +/** + * @ngdoc object + * @name ui.router.state.$uiViewScrollProvider + * + * @description + * Provider that returns the {@link ui.router.state.$uiViewScroll} service function. + */ +function $ViewScrollProvider() { + + var useAnchorScroll = false; + + /** + * @ngdoc function + * @name ui.router.state.$uiViewScrollProvider#useAnchorScroll + * @methodOf ui.router.state.$uiViewScrollProvider + * + * @description + * Reverts back to using the core [`$anchorScroll`](http://docs.angularjs.org/api/ng.$anchorScroll) service for + * scrolling based on the url anchor. + */ + this.useAnchorScroll = function () { + useAnchorScroll = true; + }; + + /** + * @ngdoc object + * @name ui.router.state.$uiViewScroll + * + * @requires $anchorScroll + * @requires $timeout + * + * @description + * When called with a jqLite element, it scrolls the element into view (after a + * `$timeout` so the DOM has time to refresh). + * + * If you prefer to rely on `$anchorScroll` to scroll the view to the anchor, + * this can be enabled by calling {@link ui.router.state.$uiViewScrollProvider#methods_useAnchorScroll `$uiViewScrollProvider.useAnchorScroll()`}. + */ + this.$get = ['$anchorScroll', '$timeout', function ($anchorScroll, $timeout) { + if (useAnchorScroll) { + return $anchorScroll; + } + + return function ($element) { + return $timeout(function () { + $element[0].scrollIntoView(); + }, 0, false); + }; + }]; +} + +angular.module('ui.router.state').provider('$uiViewScroll', $ViewScrollProvider); + +/** + * @ngdoc directive + * @name ui.router.state.directive:ui-view + * + * @requires ui.router.state.$state + * @requires $compile + * @requires $controller + * @requires $injector + * @requires ui.router.state.$uiViewScroll + * @requires $document + * + * @restrict ECA + * + * @description + * The ui-view directive tells $state where to place your templates. + * + * @param {string=} name 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. + * + * @param {string=} autoscroll It allows you to set the scroll behavior of the browser window + * when a view is populated. By default, $anchorScroll is overridden by ui-router's custom scroll + * service, {@link ui.router.state.$uiViewScroll}. This custom service let's you + * scroll ui-view elements into view when they are populated during a state activation. + * + * *Note: To revert back to old [`$anchorScroll`](http://docs.angularjs.org/api/ng.$anchorScroll) + * functionality, call `$uiViewScrollProvider.useAnchorScroll()`.* + * + * @param {string=} onload Expression to evaluate whenever the view updates. + * + * @example + * A view can be unnamed or named. + *
      + * 
      + * 
      + * + * + *
      + *
      + * + * 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: + *
      + * 
      + * $stateProvider.state("home", { + * template: "

      HELLO!

      " + * }) + *
      + * + * The above is a convenient shortcut equivalent to specifying your view explicitly with the {@link ui.router.state.$stateProvider#methods_state `views`} + * config property, by name, in this case an empty name: + *
      + * $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: + *
      + * 
      + *
      + *
      + * $stateProvider.state("home", {
      + *   views: {
      + *     "main": {
      + *       template: "

      HELLO!

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

      HELLO!

      " + * }, + * "chart": { + * template: "" + * }, + * "data": { + * template: "" + * } + * } + * }) + *
      + * + * Examples for `autoscroll`: + * + *
      + * 
      + * 
      + *
      + * 
      + * 
      + * 
      + * 
      + * 
      + * + * Resolve data: + * + * The resolved data from the state's `resolve` block is placed on the scope as `$resolve` (this + * can be customized using [[ViewDeclaration.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 usage of $resolve in a view template + *
      + * $stateProvider.state('home', {
      + *   template: '',
      + *   resolve: {
      + *     user: function(UserService) { return UserService.fetchUser(); }
      + *   }
      + * });
      + * 
      + */ +$ViewDirective.$inject = ['$state', '$injector', '$uiViewScroll', '$interpolate', '$q']; +function $ViewDirective( $state, $injector, $uiViewScroll, $interpolate, $q) { + + function getService() { + return ($injector.has) ? function(service) { + return $injector.has(service) ? $injector.get(service) : null; + } : function(service) { + try { + return $injector.get(service); + } catch (e) { + return null; + } + }; + } + + var service = getService(), + $animator = service('$animator'), + $animate = service('$animate'); + + // Returns a set of DOM manipulation functions based on which Angular version + // it should use + function getRenderer(attrs, scope) { + var statics = function() { + return { + enter: function (element, target, cb) { target.after(element); cb(); }, + leave: function (element, cb) { element.remove(); cb(); } + }; + }; + + if ($animate) { + return { + enter: function(element, target, cb) { + if (angular.version.minor > 2) { + $animate.enter(element, null, target).then(cb); + } else { + $animate.enter(element, null, target, cb); + } + }, + leave: function(element, cb) { + if (angular.version.minor > 2) { + $animate.leave(element).then(cb); + } else { + $animate.leave(element, cb); + } + } + }; + } + + if ($animator) { + var animate = $animator && $animator(scope, attrs); + + return { + enter: function(element, target, cb) {animate.enter(element, null, target); cb(); }, + leave: function(element, cb) { animate.leave(element); cb(); } + }; + } + + return statics(); + } + + var directive = { + restrict: 'ECA', + terminal: true, + priority: 400, + transclude: 'element', + compile: function (tElement, tAttrs, $transclude) { + return function (scope, $element, attrs) { + var previousEl, currentEl, currentScope, latestLocals, + onloadExp = attrs.onload || '', + autoScrollExp = attrs.autoscroll, + renderer = getRenderer(attrs, scope), + inherited = $element.inheritedData('$uiView'); + + scope.$on('$stateChangeSuccess', function() { + updateView(false); + }); + + updateView(true); + + function cleanupLastView() { + if (previousEl) { + previousEl.remove(); + previousEl = null; + } + + if (currentScope) { + currentScope.$destroy(); + currentScope = null; + } + + if (currentEl) { + var $uiViewData = currentEl.data('$uiView'); + renderer.leave(currentEl, function() { + $uiViewData.$$animLeave.resolve(); + previousEl = null; + }); + + previousEl = currentEl; + currentEl = null; + } + } + + function updateView(firstTime) { + var newScope, + name = getUiViewName(scope, attrs, inherited, $interpolate), + previousLocals = name && $state.$current && $state.$current.locals[name]; + + if (!firstTime && previousLocals === latestLocals) return; // nothing to do + newScope = scope.$new(); + latestLocals = $state.$current.locals[name]; + + /** + * @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 clone = $transclude(newScope, function(clone) { + var animEnter = $q.defer(), animLeave = $q.defer(); + var viewData = { + name: name, + $animEnter: animEnter.promise, + $animLeave: animLeave.promise, + $$animLeave: animLeave + }; + + renderer.enter(clone.data('$uiView', viewData), $element, function onUiViewEnter() { + animEnter.resolve(); + if(currentScope) { + currentScope.$emit('$viewContentAnimationEnded'); + } + + if (angular.isDefined(autoScrollExp) && !autoScrollExp || scope.$eval(autoScrollExp)) { + $uiViewScroll(clone); + } + }); + cleanupLastView(); + }); + + currentEl = clone; + 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. + * @param {string} viewName Name of the view. + */ + currentScope.$emit('$viewContentLoaded', name); + currentScope.$eval(onloadExp); + } + }; + } + }; + + return directive; +} + +$ViewDirectiveFill.$inject = ['$compile', '$controller', '$state', '$interpolate']; +function $ViewDirectiveFill ( $compile, $controller, $state, $interpolate) { + return { + restrict: 'ECA', + priority: -400, + compile: function (tElement) { + var initial = tElement.html(); + return function (scope, $element, attrs) { + var current = $state.$current, + $uiViewData = $element.data('$uiView'), + locals = current && current.locals[$uiViewData.name]; + + if (! locals) { + return; + } + + extend($uiViewData, { state: locals.$$state }); + $element.html(locals.$template ? locals.$template : initial); + + var resolveData = angular.extend({}, locals); + scope[locals.$$resolveAs] = resolveData; + + var link = $compile($element.contents()); + + if (locals.$$controller) { + locals.$scope = scope; + locals.$element = $element; + var controller = $controller(locals.$$controller, locals); + if (locals.$$controllerAs) { + scope[locals.$$controllerAs] = controller; + scope[locals.$$controllerAs][locals.$$resolveAs] = resolveData; + } + if (isFunction(controller.$onInit)) controller.$onInit(); + $element.data('$ngControllerController', controller); + $element.children().data('$ngControllerController', controller); + } + + link(scope); + }; + } + }; +} + +/** + * Shared ui-view code for both directives: + * Given scope, element, and its attributes, return the view's name + */ +function getUiViewName(scope, attrs, inherited, $interpolate) { + var name = $interpolate(attrs.uiView || attrs.name || '')(scope); + return name.indexOf('@') >= 0 ? name : (name + '@' + (inherited ? inherited.state.name : '')); +} + +angular.module('ui.router.state').directive('uiView', $ViewDirective); +angular.module('ui.router.state').directive('uiView', $ViewDirectiveFill); + +function parseStateRef(ref, current) { + var preparsed = ref.match(/^\s*({[^}]*})\s*$/), parsed; + if (preparsed) ref = current + '(' + preparsed[1] + ')'; + 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; + } +} + +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 + }; +} + +function clickHook(el, $state, $timeout, type, current) { + return function(e) { + var button = e.which || e.button, target = current(); + + 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.state, target.params, target.options); + }); + 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); + }; + } + }; +} + +function defaultOpts(el, $state) { + return { relative: stateContext(el) || $state.$current, inherit: true }; +} + +/** + * @ngdoc directive + * @name ui.router.state.directive:ui-sref + * + * @requires ui.router.state.$state + * @requires $timeout + * + * @restrict A + * + * @description + * A directive that binds a link (`` tag) to a state. If the state has an associated + * URL, the directive will automatically generate & update the `href` attribute via + * the {@link ui.router.state.$state#methods_href $state.href()} method. Clicking + * the link will trigger a state transition with optional parameters. + * + * Also middle-clicking, right-clicking, and ctrl-clicking on the link will be + * handled natively by the browser. + * + * You can also use relative state paths within ui-sref, just like the relative + * paths passed to `$state.go()`. You just need to be aware that the path is relative + * to the state that the link lives in, in other words the state that loaded the + * template containing the link. + * + * You can specify options to pass to {@link ui.router.state.$state#methods_go $state.go()} + * using the `ui-sref-opts` attribute. Options are restricted to `location`, `inherit`, + * and `reload`. + * + * @example + * Here's an example of how you'd use ui-sref and how it would compile. If you have the + * following template: + *
      + * Home | About | Next page
      + *
      + * 
      + * 
      + * + * Then the compiled html would be (assuming Html5Mode is off and current state is contacts): + *
      + * Home | About | Next page
      + *
      + * 
        + *
      • + * Joe + *
      • + *
      • + * Alice + *
      • + *
      • + * Bob + *
      • + *
      + * + * Home + *
      + * + * @param {string} ui-sref 'stateName' can be any valid absolute or relative state + * @param {Object} ui-sref-opts options to pass to {@link ui.router.state.$state#methods_go $state.go()} + */ +$StateRefDirective.$inject = ['$state', '$timeout']; +function $StateRefDirective($state, $timeout) { + return { + restrict: 'A', + require: ['?^uiSrefActive', '?^uiSrefActiveEq'], + link: function(scope, element, attrs, uiSrefActive) { + var ref = parseStateRef(attrs.uiSref, $state.current.name); + var def = { state: ref.state, href: null, params: null }; + var type = getTypeInfo(element); + var active = uiSrefActive[1] || uiSrefActive[0]; + var unlinkInfoFn = null; + var hookFn; + + def.options = extend(defaultOpts(element, $state), attrs.uiSrefOpts ? scope.$eval(attrs.uiSrefOpts) : {}); + + var update = function(val) { + if (val) def.params = angular.copy(val); + def.href = $state.href(ref.state, def.params, def.options); + + if (unlinkInfoFn) unlinkInfoFn(); + if (active) unlinkInfoFn = active.$$addStateInfo(ref.state, def.params); + if (def.href !== null) attrs.$set(type.attr, def.href); + }; + + if (ref.paramExpr) { + scope.$watch(ref.paramExpr, function(val) { if (val !== def.params) update(val); }, true); + def.params = angular.copy(scope.$eval(ref.paramExpr)); + } + update(); + + if (!type.clickable) return; + hookFn = clickHook(element, $state, $timeout, type, function() { return def; }); + element.bind("click", hookFn); + scope.$on('$destroy', function() { + element.unbind("click", hookFn); + }); + } + }; +} + +/** + * @ngdoc directive + * @name ui.router.state.directive:ui-state + * + * @requires ui.router.state.uiSref + * + * @restrict A + * + * @description + * Much like ui-sref, but will accept named $scope properties to evaluate for a state definition, + * params and override options. + * + * @param {string} ui-state 'stateName' can be any valid absolute or relative state + * @param {Object} ui-state-params params to pass to {@link ui.router.state.$state#methods_href $state.href()} + * @param {Object} ui-state-opts options to pass to {@link ui.router.state.$state#methods_go $state.go()} + */ +$StateRefDynamicDirective.$inject = ['$state', '$timeout']; +function $StateRefDynamicDirective($state, $timeout) { + return { + restrict: 'A', + require: ['?^uiSrefActive', '?^uiSrefActiveEq'], + link: function(scope, element, attrs, uiSrefActive) { + var type = getTypeInfo(element); + var active = uiSrefActive[1] || uiSrefActive[0]; + var group = [attrs.uiState, attrs.uiStateParams || null, attrs.uiStateOpts || null]; + var watch = '[' + group.map(function(val) { return val || 'null'; }).join(', ') + ']'; + var def = { state: null, params: null, options: null, href: null }; + var unlinkInfoFn = null; + var hookFn; + + function runStateRefLink (group) { + def.state = group[0]; def.params = group[1]; def.options = group[2]; + def.href = $state.href(def.state, def.params, def.options); + + if (unlinkInfoFn) unlinkInfoFn(); + if (active) unlinkInfoFn = active.$$addStateInfo(def.state, def.params); + if (def.href) attrs.$set(type.attr, def.href); + } + + scope.$watch(watch, runStateRefLink, true); + runStateRefLink(scope.$eval(watch)); + + if (!type.clickable) return; + hookFn = clickHook(element, $state, $timeout, type, function() { return def; }); + element.bind("click", hookFn); + scope.$on('$destroy', function() { + element.unbind("click", hookFn); + }); + } + }; +} + + +/** + * @ngdoc directive + * @name ui.router.state.directive:ui-sref-active + * + * @requires ui.router.state.$state + * @requires ui.router.state.$stateParams + * @requires $interpolate + * + * @restrict A + * + * @description + * A directive working alongside ui-sref to add classes to an element when the + * related ui-sref directive's state is active, and removing them when it is inactive. + * The primary use-case is to simplify the special appearance of navigation menus + * relying on `ui-sref`, by having the "active" state's menu button appear different, + * distinguishing it from the inactive menu items. + * + * ui-sref-active can live on the same element as ui-sref or on a parent element. The first + * ui-sref-active found at the same level or above the ui-sref will be used. + * + * Will activate when the ui-sref's target state or any child state is active. If you + * need to activate only when the ui-sref target state is active and *not* any of + * it's children, then you will use + * {@link ui.router.state.directive:ui-sref-active-eq ui-sref-active-eq} + * + * @example + * Given the following template: + *
      + * 
      + * 
      + * + * + * When the app state is "app.user" (or any children states), and contains the state parameter "user" with value "bilbobaggins", + * the resulting HTML will appear as (note the 'active' class): + *
      + * 
      + * 
      + * + * 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: + *
      + * 
        + *
      • + * link + *
      • + *
      + *
      + * + * It is also possible to pass ui-sref-active an expression that evaluates + * to an object hash, whose keys represent active class names and whose + * 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: + *
      + * 
      + * 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 shadow the state provided by ui-sref. + */ + +/** + * @ngdoc directive + * @name ui.router.state.directive:ui-sref-active-eq + * + * @requires ui.router.state.$state + * @requires ui.router.state.$stateParams + * @requires $interpolate + * + * @restrict A + * + * @description + * The same as {@link ui.router.state.directive:ui-sref-active ui-sref-active} but will only activate + * when the exact target state used in the `ui-sref` is active; no child states. + * + */ +$StateRefActiveDirective.$inject = ['$state', '$stateParams', '$interpolate']; +function $StateRefActiveDirective($state, $stateParams, $interpolate) { + return { + restrict: "A", + controller: ['$scope', '$element', '$attrs', '$timeout', function ($scope, $element, $attrs, $timeout) { + var states = [], activeClasses = {}, 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, $state.current.name); + 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; + }; + + $scope.$on('$stateChangeSuccess', update); + + function addState(stateName, stateParams, activeClass) { + var state = $state.get(stateName, stateContext($element)); + var stateHash = createStateHash(stateName, stateParams); + + var stateInfo = { + state: state || { name: stateName }, + params: stateParams, + hash: stateHash + }; + + states.push(stateInfo); + activeClasses[stateHash] = activeClass; + + return function removeState() { + var idx = states.indexOf(stateInfo); + if (idx !== -1) states.splice(idx, 1); + }; + } + + /** + * @param {string} state + * @param {Object|string} [params] + * @return {string} + */ + function createStateHash(state, params) { + if (!isString(state)) { + throw new Error('state should be a string'); + } + if (isObject(params)) { + return state + toJson(params); + } + params = $scope.$eval(params); + if (isObject(params)) { + return state + toJson(params); + } + return state; + } + + // Update route state + function update() { + for (var i = 0; i < states.length; i++) { + if (anyMatch(states[i].state, states[i].params)) { + addClass($element, activeClasses[states[i].hash]); + } else { + removeClass($element, activeClasses[states[i].hash]); + } + + if (exactMatch(states[i].state, states[i].params)) { + addClass($element, activeEqClass); + } else { + removeClass($element, activeEqClass); + } + } + } + + function addClass(el, className) { $timeout(function () { el.addClass(className); }); } + function removeClass(el, className) { el.removeClass(className); } + function anyMatch(state, params) { return $state.includes(state.name, params); } + function exactMatch(state, params) { return $state.is(state.name, params); } + + update(); + }] + }; +} + +angular.module('ui.router.state') + .directive('uiSref', $StateRefDirective) + .directive('uiSrefActive', $StateRefActiveDirective) + .directive('uiSrefActiveEq', $StateRefActiveDirective) + .directive('uiState', $StateRefDynamicDirective); + +/** + * @ngdoc filter + * @name ui.router.state.filter:isState + * + * @requires ui.router.state.$state + * + * @description + * Translates to {@link ui.router.state.$state#methods_is $state.is("stateName")}. + */ +$IsStateFilter.$inject = ['$state']; +function $IsStateFilter($state) { + var isFilter = function (state, params) { + return $state.is(state, params); + }; + isFilter.$stateful = true; + return isFilter; +} + +/** + * @ngdoc filter + * @name ui.router.state.filter:includedByState + * + * @requires ui.router.state.$state + * + * @description + * Translates to {@link ui.router.state.$state#methods_includes $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; +} + +angular.module('ui.router.state') + .filter('isState', $IsStateFilter) + .filter('includedByState', $IncludedByStateFilter); +})(window, window.angular); +/** + * angular-ui-sortable - This directive allows you to jQueryUI Sortable. + * @version v0.14.0 - 2016-03-28 + * @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',{ + // 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', + scope: { + ngModel: '=', + uiSortable: '=' + }, + link: function(scope, element, attrs, ngModel) { + var savedNodes; + + function combineCallbacks(first, second){ + var firstIsFunc = first && (typeof first === 'function'); + var secondIsFunc = second && (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) { + if (key === 'items') { + value = uiSortableConfig.items; + } else if (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) + // is placed last. + result = savedNodes.last(); + } + return result; + } + + // thanks jquery-ui + function isFloating (item) { + return (/left|right/).test(item.css('float')) || (/inline|table-cell/).test(item.css('display')); + } + + function getElementScope(elementScopes, element) { + var result = null; + for (var i = 0; i < elementScopes.length; i++) { + var x = elementScopes[i]; + if (x.element[0] === element[0]) { + result = x.scope; + break; + } + } + return result; + } + + 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(ui) { + return ui.item.parent() + .find(opts['ui-model-items']) + .index(ui.item); + } + + var opts = {}; + + // directive specific options + var directiveOpts = { + 'ui-floating': undefined, + 'ui-model-items': uiSortableConfig.items + }; + + var callbacks = { + receive: null, + remove: null, + start: null, + stop: null, + update: null + }; + + var wrappers = { + helper: null + }; + + 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) { + 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); + ui.item.sortable = { + model: ngModel.$modelValue[index], + index: index, + source: 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; + }); + } + }; + }; + + callbacks.activate = function(e, ui) { + // 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 = element.contents(); + + // 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); + } + + // save the directive's scope so that it is accessible from ui.item.sortable + var connectedSortables = ui.item.sortable._connectedSortables || []; + + connectedSortables.push({ + element: element, + scope: scope + }); + + ui.item.sortable._connectedSortables = connectedSortables; + }; + + callbacks.update = function(e, ui) { + // 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); + var droptarget = ui.item.parent(); + ui.item.sortable.droptarget = droptarget; + + var droptargetScope = getElementScope(ui.item.sortable._connectedSortables, droptarget); + ui.item.sortable.droptargetModel = droptargetScope.ngModel; + + // 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); + } + savedNodes.appendTo(element); + + // 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); + }); + } + }; + + callbacks.stop = function(e, ui) { + // 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. + if(!ui.item.sortable.received && + ('dropindex' in ui.item.sortable) && + !ui.item.sortable.isCanceled()) { + + scope.$apply(function () { + ngModel.$modelValue.splice( + ui.item.sortable.dropindex, 0, + ngModel.$modelValue.splice(ui.item.sortable.index, 1)[0]); + }); + } else { + // if the item was not moved, then restore the elements + // so that the ngRepeat's comment are correct. + if ((!('dropindex' in ui.item.sortable) || ui.item.sortable.isCanceled()) && + !angular.equals(element.contents(), savedNodes)) { + + 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); + } + savedNodes.appendTo(element); + } + } + + // It's now safe to clear the savedNodes + // since stop is the last callback. + savedNodes = 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]; + }); + } + }; + + wrappers.helper = function (inner) { + if (inner && typeof inner === 'function') { + return function (e, item) { + var innerResult = inner.apply(this, arguments); + 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); + } + } + }; + } + ]); + +})(window, window.angular); + +/** + * @license Angular UI Tree v2.15.0 + * (c) 2010-2016. https://github.com/angular-ui-tree/angular-ui-tree + * License: MIT + */ +(function () { + 'use strict'; + + angular.module('ui.tree', []) + .constant('treeConfig', { + treeClass: 'angular-ui-tree', + emptyTreeClass: 'angular-ui-tree-empty', + hiddenClass: 'angular-ui-tree-hidden', + nodesClass: 'angular-ui-tree-nodes', + nodeClass: 'angular-ui-tree-node', + handleClass: 'angular-ui-tree-handle', + placeholderClass: 'angular-ui-tree-placeholder', + dragClass: 'angular-ui-tree-drag', + dragThreshold: 3, + levelThreshold: 30, + defaultCollapsed: false + }); + +})(); + +(function () { + 'use strict'; + + angular.module('ui.tree') + + .controller('TreeHandleController', ['$scope', '$element', + function ($scope, $element) { + this.scope = $scope; + + $scope.$element = $element; + $scope.$nodeScope = null; + $scope.$type = 'uiTreeHandle'; + + } + ]); +})(); + +(function () { + 'use strict'; + + angular.module('ui.tree') + .controller('TreeNodeController', ['$scope', '$element', + function ($scope, $element) { + this.scope = $scope; + + $scope.$element = $element; + $scope.$modelValue = null; // Model value for node; + $scope.$parentNodeScope = null; // uiTreeNode Scope of parent node; + $scope.$childNodesScope = null; // uiTreeNodes Scope of child nodes. + $scope.$parentNodesScope = null; // uiTreeNodes Scope of parent nodes. + $scope.$treeScope = null; // uiTree scope + $scope.$handleScope = null; // it's handle scope + $scope.$type = 'uiTreeNode'; + $scope.$$allowNodeDrop = false; + $scope.collapsed = false; + + $scope.init = function (controllersArr) { + var treeNodesCtrl = controllersArr[0]; + $scope.$treeScope = controllersArr[1] ? controllersArr[1].scope : null; + + // find the scope of it's parent node + $scope.$parentNodeScope = treeNodesCtrl.scope.$nodeScope; + // modelValue for current node + $scope.$modelValue = treeNodesCtrl.scope.$modelValue[$scope.$index]; + $scope.$parentNodesScope = treeNodesCtrl.scope; + treeNodesCtrl.scope.initSubNode($scope); // init sub nodes + + $element.on('$destroy', function () { + treeNodesCtrl.scope.destroySubNode($scope); // destroy sub nodes + }); + }; + + $scope.index = function () { + return $scope.$parentNodesScope.$modelValue.indexOf($scope.$modelValue); + }; + + $scope.dragEnabled = function () { + return !($scope.$treeScope && !$scope.$treeScope.dragEnabled); + }; + + $scope.isSibling = function (targetNode) { + return $scope.$parentNodesScope == targetNode.$parentNodesScope; + }; + + $scope.isChild = function (targetNode) { + var nodes = $scope.childNodes(); + return nodes && nodes.indexOf(targetNode) > -1; + }; + + $scope.prev = function () { + var index = $scope.index(); + if (index > 0) { + return $scope.siblings()[index - 1]; + } + return null; + }; + + $scope.siblings = function () { + return $scope.$parentNodesScope.childNodes(); + }; + + $scope.childNodesCount = function () { + return $scope.childNodes() ? $scope.childNodes().length : 0; + }; + + $scope.hasChild = function () { + return $scope.childNodesCount() > 0; + }; + + $scope.childNodes = function () { + return $scope.$childNodesScope && $scope.$childNodesScope.$modelValue ? + $scope.$childNodesScope.childNodes() : + null; + }; + + $scope.accept = function (sourceNode, destIndex) { + return $scope.$childNodesScope && + $scope.$childNodesScope.$modelValue && + $scope.$childNodesScope.accept(sourceNode, destIndex); + }; + + $scope.remove = function () { + return $scope.$parentNodesScope.removeNode($scope); + }; + + $scope.toggle = function () { + $scope.collapsed = !$scope.collapsed; + }; + + $scope.collapse = function () { + $scope.collapsed = true; + }; + + $scope.expand = function () { + $scope.collapsed = false; + }; + + $scope.depth = function () { + var parentNode = $scope.$parentNodeScope; + if (parentNode) { + return parentNode.depth() + 1; + } + return 1; + }; + + /** + * Returns the depth of the deepest subtree under this node + * @param scope a TreeNodesController scope object + * @returns Depth of all nodes *beneath* this node. If scope belongs to a leaf node, the + * result is 0 (it has no subtree). + */ + function countSubTreeDepth(scope) { + var thisLevelDepth = 0, + childNodes = scope.childNodes(), + childNode, + childDepth, + i; + if (!childNodes || childNodes.length === 0) { + return 0; + } + for (i = childNodes.length - 1; i >= 0 ; i--) { + childNode = childNodes[i], + childDepth = 1 + countSubTreeDepth(childNode); + thisLevelDepth = Math.max(thisLevelDepth, childDepth); + } + return thisLevelDepth; + } + + $scope.maxSubDepth = function () { + return $scope.$childNodesScope ? countSubTreeDepth($scope.$childNodesScope) : 0; + }; + } + ]); +})(); + +(function () { + 'use strict'; + + angular.module('ui.tree') + + .controller('TreeNodesController', ['$scope', '$element', + function ($scope, $element) { + this.scope = $scope; + + $scope.$element = $element; + $scope.$modelValue = null; + $scope.$nodeScope = null; // the scope of node which the nodes belongs to + $scope.$treeScope = null; + $scope.$type = 'uiTreeNodes'; + $scope.$nodesMap = {}; + + $scope.nodropEnabled = false; + $scope.maxDepth = 0; + $scope.cloneEnabled = false; + + $scope.initSubNode = function (subNode) { + if (!subNode.$modelValue) { + return null; + } + $scope.$nodesMap[subNode.$modelValue.$$hashKey] = subNode; + }; + + $scope.destroySubNode = function (subNode) { + if (!subNode.$modelValue) { + return null; + } + $scope.$nodesMap[subNode.$modelValue.$$hashKey] = null; + }; + + $scope.accept = function (sourceNode, destIndex) { + return $scope.$treeScope.$callbacks.accept(sourceNode, $scope, destIndex); + }; + + $scope.beforeDrag = function (sourceNode) { + return $scope.$treeScope.$callbacks.beforeDrag(sourceNode); + }; + + $scope.isParent = function (node) { + return node.$parentNodesScope == $scope; + }; + + $scope.hasChild = function () { + return $scope.$modelValue.length > 0; + }; + + $scope.safeApply = function (fn) { + var phase = this.$root.$$phase; + if (phase == '$apply' || phase == '$digest') { + if (fn && (typeof (fn) === 'function')) { + fn(); + } + } else { + this.$apply(fn); + } + }; + + $scope.removeNode = function (node) { + var index = $scope.$modelValue.indexOf(node.$modelValue); + if (index > -1) { + $scope.safeApply(function () { + $scope.$modelValue.splice(index, 1)[0]; + }); + return $scope.$treeScope.$callbacks.removed(node); + } + return null; + }; + + $scope.insertNode = function (index, nodeData) { + $scope.safeApply(function () { + $scope.$modelValue.splice(index, 0, nodeData); + }); + }; + + $scope.childNodes = function () { + var i, nodes = []; + if ($scope.$modelValue) { + for (i = 0; i < $scope.$modelValue.length; i++) { + nodes.push($scope.$nodesMap[$scope.$modelValue[i].$$hashKey]); + } + } + return nodes; + }; + + $scope.depth = function () { + if ($scope.$nodeScope) { + return $scope.$nodeScope.depth(); + } + return 0; // if it has no $nodeScope, it's root + }; + + // check if depth limit has reached + $scope.outOfDepth = function (sourceNode) { + var maxDepth = $scope.maxDepth || $scope.$treeScope.maxDepth; + if (maxDepth > 0) { + return $scope.depth() + sourceNode.maxSubDepth() + 1 > maxDepth; + } + return false; + }; + + } + ]); +})(); + +(function () { + 'use strict'; + + angular.module('ui.tree') + + .controller('TreeController', ['$scope', '$element', + function ($scope, $element) { + this.scope = $scope; + + $scope.$element = $element; + $scope.$nodesScope = null; // root nodes + $scope.$type = 'uiTree'; + $scope.$emptyElm = null; + $scope.$callbacks = null; + + $scope.dragEnabled = true; + $scope.emptyPlaceholderEnabled = true; + $scope.maxDepth = 0; + $scope.dragDelay = 0; + $scope.cloneEnabled = false; + $scope.nodropEnabled = false; + + // Check if it's a empty tree + $scope.isEmpty = function () { + return ($scope.$nodesScope && $scope.$nodesScope.$modelValue + && $scope.$nodesScope.$modelValue.length === 0); + }; + + // add placeholder to empty tree + $scope.place = function (placeElm) { + $scope.$nodesScope.$element.append(placeElm); + $scope.$emptyElm.remove(); + }; + + this.resetEmptyElement = function () { + if ((!$scope.$nodesScope.$modelValue || $scope.$nodesScope.$modelValue.length === 0) && + $scope.emptyPlaceholderEnabled) { + $element.append($scope.$emptyElm); + } else { + $scope.$emptyElm.remove(); + } + }; + + $scope.resetEmptyElement = this.resetEmptyElement; + } + ]); +})(); + +(function () { + 'use strict'; + + angular.module('ui.tree') + .directive('uiTree', ['treeConfig', '$window', + function (treeConfig, $window) { + return { + restrict: 'A', + scope: true, + controller: 'TreeController', + link: function (scope, element, attrs, ctrl) { + var callbacks = { + accept: null, + beforeDrag: null + }, + config = {}, + tdElm, + $trElm, + emptyElmColspan; + + angular.extend(config, treeConfig); + if (config.treeClass) { + element.addClass(config.treeClass); + } + + if (element.prop('tagName').toLowerCase() === 'table') { + scope.$emptyElm = angular.element($window.document.createElement('tr')); + $trElm = element.find('tr'); + // If we can find a tr, then we can use its td children as the empty element colspan. + if ($trElm.length > 0) { + emptyElmColspan = angular.element($trElm).children().length; + } else { + // If not, by setting a huge colspan we make sure it takes full width. + emptyElmColspan = 1000000; + } + tdElm = angular.element($window.document.createElement('td')) + .attr('colspan', emptyElmColspan); + scope.$emptyElm.append(tdElm); + } else { + scope.$emptyElm = angular.element($window.document.createElement('div')); + } + + if (config.emptyTreeClass) { + scope.$emptyElm.addClass(config.emptyTreeClass); + } + + scope.$watch('$nodesScope.$modelValue.length', function (val) { + if (!angular.isNumber(val)) { + return; + } + + ctrl.resetEmptyElement(); + }, true); + + scope.$watch(attrs.dragEnabled, function (val) { + if ((typeof val) == 'boolean') { + scope.dragEnabled = val; + } + }); + + scope.$watch(attrs.emptyPlaceholderEnabled, function (val) { + if ((typeof val) == 'boolean') { + scope.emptyPlaceholderEnabled = val; + ctrl.resetEmptyElement(); + } + }); + + scope.$watch(attrs.nodropEnabled, function (val) { + if ((typeof val) == 'boolean') { + scope.nodropEnabled = val; + } + }); + + scope.$watch(attrs.cloneEnabled, function (val) { + if ((typeof val) == 'boolean') { + scope.cloneEnabled = val; + } + }); + + scope.$watch(attrs.maxDepth, function (val) { + if ((typeof val) == 'number') { + scope.maxDepth = val; + } + }); + + scope.$watch(attrs.dragDelay, function (val) { + if ((typeof val) == 'number') { + scope.dragDelay = val; + } + }); + + /** + * Callback checks if the destination node can accept the dragged node. + * By default, ui-tree will check that 'data-nodrop-enabled' is not set for the + * destination ui-tree-nodes, and that the 'max-depth' attribute will not be exceeded + * if it is set on the ui-tree or ui-tree-nodes. + * This callback can be overridden, but callers must manually enforce nodrop and max-depth + * themselves if they need those to be enforced. + * @param sourceNodeScope Scope of the ui-tree-node being dragged + * @param destNodesScope Scope of the ui-tree-nodes where the node is hovering + * @param destIndex Index in the destination nodes array where the source node will drop + * @returns {boolean} True if the node is permitted to be dropped here + */ + callbacks.accept = function (sourceNodeScope, destNodesScope, destIndex) { + return !(destNodesScope.nodropEnabled || destNodesScope.$treeScope.nodropEnabled || destNodesScope.outOfDepth(sourceNodeScope)); + }; + + callbacks.beforeDrag = function (sourceNodeScope) { + return true; + }; + + callbacks.removed = function (node) { + + }; + + /** + * Callback is fired when a node is successfully dropped in a new location + * @param event + */ + callbacks.dropped = function (event) { + + }; + + /** + * Callback is fired each time the user starts dragging a node + * @param event + */ + callbacks.dragStart = function (event) { + + }; + + /** + * Callback is fired each time a dragged node is moved with the mouse/touch. + * @param event + */ + callbacks.dragMove = function (event) { + + }; + + /** + * Callback is fired when the tree exits drag mode. If the user dropped a node, the drop may have been + * accepted or reverted. + * @param event + */ + callbacks.dragStop = function (event) { + + }; + + /** + * Callback is fired when a user drops a node (but prior to processing the drop action) + * beforeDrop can return a Promise, truthy, or falsy (returning nothing is falsy). + * If it returns falsy, or a resolve Promise, the node move is accepted + * If it returns truthy, or a rejected Promise, the node move is reverted + * @param event + * @returns {Boolean|Promise} Truthy (or rejected Promise) to cancel node move; falsy (or resolved promise) + */ + callbacks.beforeDrop = function (event) { + + }; + + scope.$watch(attrs.uiTree, function (newVal, oldVal) { + angular.forEach(newVal, function (value, key) { + if (callbacks[key]) { + if (typeof value === 'function') { + callbacks[key] = value; + } + } + }); + + scope.$callbacks = callbacks; + }, true); + + + } + }; + } + ]); +})(); + +(function () { + 'use strict'; + + angular.module('ui.tree') + .directive('uiTreeHandle', ['treeConfig', + function (treeConfig) { + return { + require: '^uiTreeNode', + restrict: 'A', + scope: true, + controller: 'TreeHandleController', + link: function (scope, element, attrs, treeNodeCtrl) { + var config = {}; + angular.extend(config, treeConfig); + if (config.handleClass) { + element.addClass(config.handleClass); + } + // connect with the tree node. + if (scope != treeNodeCtrl.scope) { + scope.$nodeScope = treeNodeCtrl.scope; + treeNodeCtrl.scope.$handleScope = scope; + } + } + }; + } + ]); +})(); + +(function () { + 'use strict'; + + angular.module('ui.tree') + + .directive('uiTreeNode', ['treeConfig', 'UiTreeHelper', '$window', '$document', '$timeout', '$q', '$rootElement', + function (treeConfig, UiTreeHelper, $window, $document, $timeout, $q, $rootElement) { + return { + require: ['^uiTreeNodes', '^uiTree'], + restrict: 'A', + controller: 'TreeNodeController', + link: function (scope, element, attrs, controllersArr) { + // todo startPos is unused + var config = {}, + hasTouch = 'ontouchstart' in window, + startPos, firstMoving, dragInfo, pos, + placeElm, hiddenPlaceElm, dragElm, + treeScope = null, + elements, // As a parameter for callbacks + dragDelaying = true, + dragStarted = false, + dragTimer = null, + body = document.body, + html = document.documentElement, + document_height, + document_width, + dragStart, + tagName, + dragMove, + dragEnd, + dragStartEvent, + dragMoveEvent, + dragEndEvent, + dragCancelEvent, + dragDelay, + bindDragStartEvents, + bindDragMoveEvents, + unbindDragMoveEvents, + keydownHandler, + outOfBounds, + isHandleChild, + el; + + angular.extend(config, treeConfig); + if (config.nodeClass) { + element.addClass(config.nodeClass); + } + scope.init(controllersArr); + + scope.collapsed = !!UiTreeHelper.getNodeAttribute(scope, 'collapsed') || treeConfig.defaultCollapsed; + scope.sourceOnly = scope.nodropEnabled || scope.$treeScope.nodropEnabled; + + scope.$watch(attrs.collapsed, function (val) { + if ((typeof val) == 'boolean') { + scope.collapsed = val; + } + }); + + scope.$watch('collapsed', function (val) { + UiTreeHelper.setNodeAttribute(scope, 'collapsed', val); + attrs.$set('collapsed', val); + }); + + scope.$on('angular-ui-tree:collapse-all', function () { + scope.collapsed = true; + }); + + scope.$on('angular-ui-tree:expand-all', function () { + scope.collapsed = false; + }); + + /** + * Called when the user has grabbed a node and started dragging it + * @param e + */ + dragStart = function (e) { + // disable right click + if (!hasTouch && (e.button === 2 || e.which === 3)) { + return; + } + + // event has already fired in other scope + if (e.uiTreeDragging || (e.originalEvent && e.originalEvent.uiTreeDragging)) { + return; + } + + // the node being dragged + var eventElm = angular.element(e.target), + isHandleChild, cloneElm, eventElmTagName, tagName, + eventObj, tdElm, hStyle, + isTreeNode, + isTreeNodeHandle; + + // if the target element is a child element of a ui-tree-handle, + // use the containing handle element as target element + isHandleChild = UiTreeHelper.treeNodeHandlerContainerOfElement(eventElm); + if (isHandleChild) { + eventElm = angular.element(isHandleChild); + } + + cloneElm = element.clone(); + isTreeNode = UiTreeHelper.elementIsTreeNode(eventElm); + isTreeNodeHandle = UiTreeHelper.elementIsTreeNodeHandle(eventElm); + + if (!isTreeNode && !isTreeNodeHandle) { + return; + } + + if (isTreeNode && UiTreeHelper.elementContainsTreeNodeHandler(eventElm)) { + return; + } + + eventElmTagName = eventElm.prop('tagName').toLowerCase(); + if (eventElmTagName == 'input' || + eventElmTagName == 'textarea' || + eventElmTagName == 'button' || + eventElmTagName == 'select') { // if it's a input or button, ignore it + return; + } + + // check if it or it's parents has a 'data-nodrag' attribute + el = angular.element(e.target); + while (el && el[0] && el[0] !== element) { + if (UiTreeHelper.nodrag(el)) { // if the node mark as `nodrag`, DONOT drag it. + return; + } + el = el.parent(); + } + + if (!scope.beforeDrag(scope)) { + return; + } + + e.uiTreeDragging = true; // stop event bubbling + if (e.originalEvent) { + e.originalEvent.uiTreeDragging = true; + } + e.preventDefault(); + eventObj = UiTreeHelper.eventObj(e); + + firstMoving = true; + dragInfo = UiTreeHelper.dragInfo(scope); + + tagName = element.prop('tagName'); + + if (tagName.toLowerCase() === 'tr') { + placeElm = angular.element($window.document.createElement(tagName)); + tdElm = angular.element($window.document.createElement('td')) + .addClass(config.placeholderClass) + .attr('colspan', element[0].children.length); + placeElm.append(tdElm); + } else { + placeElm = angular.element($window.document.createElement(tagName)) + .addClass(config.placeholderClass); + } + hiddenPlaceElm = angular.element($window.document.createElement(tagName)); + if (config.hiddenClass) { + hiddenPlaceElm.addClass(config.hiddenClass); + } + + pos = UiTreeHelper.positionStarted(eventObj, element); + placeElm.css('height', UiTreeHelper.height(element) + 'px'); + + dragElm = angular.element($window.document.createElement(scope.$parentNodesScope.$element.prop('tagName'))) + .addClass(scope.$parentNodesScope.$element.attr('class')).addClass(config.dragClass); + dragElm.css('width', UiTreeHelper.width(element) + 'px'); + dragElm.css('z-index', 9999); + + // Prevents cursor to change rapidly in Opera 12.16 and IE when dragging an element + hStyle = (element[0].querySelector('.angular-ui-tree-handle') || element[0]).currentStyle; + if (hStyle) { + document.body.setAttribute('ui-tree-cursor', $document.find('body').css('cursor') || ''); + $document.find('body').css({'cursor': hStyle.cursor + '!important'}); + } + + if (scope.sourceOnly) { + placeElm.css('display', 'none'); + } + element.after(placeElm); + element.after(hiddenPlaceElm); + if (dragInfo.isClone() && scope.sourceOnly) { + dragElm.append(cloneElm); + } else { + dragElm.append(element); + } + + $rootElement.append(dragElm); + + dragElm.css({ + 'left': eventObj.pageX - pos.offsetX + 'px', + 'top': eventObj.pageY - pos.offsetY + 'px' + }); + elements = { + placeholder: placeElm, + dragging: dragElm + }; + + bindDragMoveEvents(); + // Fire dragStart callback + scope.$apply(function () { + scope.$treeScope.$callbacks.dragStart(dragInfo.eventArgs(elements, pos)); + }); + + document_height = Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight); + document_width = Math.max(body.scrollWidth, body.offsetWidth, html.clientWidth, html.scrollWidth, html.offsetWidth); + }; + + dragMove = function (e) { + var eventObj = UiTreeHelper.eventObj(e), + prev, + next, + leftElmPos, + topElmPos, + top_scroll, + bottom_scroll, + target, + decrease, + targetX, + targetY, + displayElm, + targetNode, + targetElm, + isEmpty, + targetOffset, + targetBefore; + + if (dragElm) { + e.preventDefault(); + + if ($window.getSelection) { + $window.getSelection().removeAllRanges(); + } else if ($window.document.selection) { + $window.document.selection.empty(); + } + + leftElmPos = eventObj.pageX - pos.offsetX; + topElmPos = eventObj.pageY - pos.offsetY; + + //dragElm can't leave the screen on the left + if (leftElmPos < 0) { + leftElmPos = 0; + } + + //dragElm can't leave the screen on the top + if (topElmPos < 0) { + topElmPos = 0; + } + + //dragElm can't leave the screen on the bottom + if ((topElmPos + 10) > document_height) { + topElmPos = document_height - 10; + } + + //dragElm can't leave the screen on the right + if ((leftElmPos + 10) > document_width) { + leftElmPos = document_width - 10; + } + + dragElm.css({ + 'left': leftElmPos + 'px', + 'top': topElmPos + 'px' + }); + + top_scroll = window.pageYOffset || $window.document.documentElement.scrollTop; + bottom_scroll = top_scroll + (window.innerHeight || $window.document.clientHeight || $window.document.clientHeight); + + // to scroll down if cursor y-position is greater than the bottom position the vertical scroll + if (bottom_scroll < eventObj.pageY && bottom_scroll <= document_height) { + window.scrollBy(0, 10); + } + + // to scroll top if cursor y-position is less than the top position the vertical scroll + if (top_scroll > eventObj.pageY) { + window.scrollBy(0, -10); + } + + UiTreeHelper.positionMoved(e, pos, firstMoving); + if (firstMoving) { + firstMoving = false; + return; + } + + // check if add it as a child node first + // todo decrease is unused + decrease = (UiTreeHelper.offset(dragElm).left - UiTreeHelper.offset(placeElm).left) >= config.threshold; + + targetX = eventObj.pageX - ($window.pageXOffset || + $window.document.body.scrollLeft || + $window.document.documentElement.scrollLeft) - + ($window.document.documentElement.clientLeft || 0); + + targetY = eventObj.pageY - ($window.pageYOffset || + $window.document.body.scrollTop || + $window.document.documentElement.scrollTop) - + ($window.document.documentElement.clientTop || 0); + + // Select the drag target. Because IE does not support CSS 'pointer-events: none', it will always + // pick the drag element itself as the target. To prevent this, we hide the drag element while + // selecting the target. + if (angular.isFunction(dragElm.hide)) { + dragElm.hide(); + } else { + displayElm = dragElm[0].style.display; + dragElm[0].style.display = 'none'; + } + + // when using elementFromPoint() inside an iframe, you have to call + // elementFromPoint() twice to make sure IE8 returns the correct value + $window.document.elementFromPoint(targetX, targetY); + + targetElm = angular.element($window.document.elementFromPoint(targetX, targetY)); + + // if the target element is a child element of a ui-tree-handle, + // use the containing handle element as target element + isHandleChild = UiTreeHelper.treeNodeHandlerContainerOfElement(targetElm); + if (isHandleChild) { + targetElm = angular.element(isHandleChild); + } + + if (angular.isFunction(dragElm.show)) { + dragElm.show(); + } else { + dragElm[0].style.display = displayElm; + } + + outOfBounds = !UiTreeHelper.elementIsTreeNodeHandle(targetElm) && + !UiTreeHelper.elementIsTreeNode(targetElm) && + !UiTreeHelper.elementIsTreeNodes(targetElm) && + !UiTreeHelper.elementIsTree(targetElm) && + !UiTreeHelper.elementIsPlaceholder(targetElm); + + // Detect out of bounds condition, update drop target display, and prevent drop + if (outOfBounds) { + + // Remove the placeholder + placeElm.remove(); + + // If the target was an empty tree, replace the empty element placeholder + if (treeScope) { + treeScope.resetEmptyElement(); + treeScope = null; + } + } + + // move horizontal + if (pos.dirAx && pos.distAxX >= config.levelThreshold) { + pos.distAxX = 0; + + // increase horizontal level if previous sibling exists and is not collapsed + if (pos.distX > 0) { + prev = dragInfo.prev(); + if (prev && !prev.collapsed + && prev.accept(scope, prev.childNodesCount())) { + prev.$childNodesScope.$element.append(placeElm); + dragInfo.moveTo(prev.$childNodesScope, prev.childNodes(), prev.childNodesCount()); + } + } + + // decrease horizontal level + if (pos.distX < 0) { + // we can't decrease a level if an item preceeds the current one + next = dragInfo.next(); + if (!next) { + target = dragInfo.parentNode(); // As a sibling of it's parent node + if (target + && target.$parentNodesScope.accept(scope, target.index() + 1)) { + target.$element.after(placeElm); + dragInfo.moveTo(target.$parentNodesScope, target.siblings(), target.index() + 1); + } + } + } + } + + // move vertical + if (!pos.dirAx) { + if (UiTreeHelper.elementIsTree(targetElm)) { + targetNode = targetElm.controller('uiTree').scope; + } else if (UiTreeHelper.elementIsTreeNodeHandle(targetElm)) { + targetNode = targetElm.controller('uiTreeHandle').scope; + } else if (UiTreeHelper.elementIsTreeNode(targetElm)) { + targetNode = targetElm.controller('uiTreeNode').scope; + } else if (UiTreeHelper.elementIsTreeNodes(targetElm)) { + targetNode = targetElm.controller('uiTreeNodes').scope; + } else if (UiTreeHelper.elementIsPlaceholder(targetElm)) { + targetNode = targetElm.controller('uiTreeNodes').scope; + } else if (targetElm.controller('uiTreeNode')) { + // is a child element of a node + targetNode = targetElm.controller('uiTreeNode').scope; + } + + // check it's new position + isEmpty = false; + if (!targetNode) { + return; + } + + // Show the placeholder if it was hidden for nodrop-enabled and this is a new tree + if (targetNode.$treeScope && !targetNode.$parent.nodropEnabled && !targetNode.$treeScope.nodropEnabled) { + placeElm.css('display', ''); + } + + if (targetNode.$type == 'uiTree' && targetNode.dragEnabled) { + isEmpty = targetNode.isEmpty(); // Check if it's empty tree + } + + if (targetNode.$type == 'uiTreeHandle') { + targetNode = targetNode.$nodeScope; + } + + if (targetNode.$type != 'uiTreeNode' + && !isEmpty) { // Check if it is a uiTreeNode or it's an empty tree + return; + } + + // if placeholder move from empty tree, reset it. + if (treeScope && placeElm.parent()[0] != treeScope.$element[0]) { + treeScope.resetEmptyElement(); + treeScope = null; + } + + if (isEmpty) { // it's an empty tree + treeScope = targetNode; + if (targetNode.$nodesScope.accept(scope, 0)) { + targetNode.place(placeElm); + dragInfo.moveTo(targetNode.$nodesScope, targetNode.$nodesScope.childNodes(), 0); + } + } else if (targetNode.dragEnabled()) { // drag enabled + targetElm = targetNode.$element; // Get the element of ui-tree-node + targetOffset = UiTreeHelper.offset(targetElm); + targetBefore = targetNode.horizontal ? eventObj.pageX < (targetOffset.left + UiTreeHelper.width(targetElm) / 2) + : eventObj.pageY < (targetOffset.top + UiTreeHelper.height(targetElm) / 2); + + if (targetNode.$parentNodesScope.accept(scope, targetNode.index())) { + if (targetBefore) { + targetElm[0].parentNode.insertBefore(placeElm[0], targetElm[0]); + dragInfo.moveTo(targetNode.$parentNodesScope, targetNode.siblings(), targetNode.index()); + } else { + targetElm.after(placeElm); + dragInfo.moveTo(targetNode.$parentNodesScope, targetNode.siblings(), targetNode.index() + 1); + } + } else if (!targetBefore && targetNode.accept(scope, targetNode.childNodesCount())) { // we have to check if it can add the dragging node as a child + targetNode.$childNodesScope.$element.append(placeElm); + dragInfo.moveTo(targetNode.$childNodesScope, targetNode.childNodes(), targetNode.childNodesCount()); + } else { + outOfBounds = true; + } + } + } + + scope.$apply(function () { + scope.$treeScope.$callbacks.dragMove(dragInfo.eventArgs(elements, pos)); + }); + } + }; + + dragEnd = function (e) { + var dragEventArgs = dragInfo.eventArgs(elements, pos); + e.preventDefault(); + unbindDragMoveEvents(); + + scope.$treeScope.$apply(function () { + $q.when(scope.$treeScope.$callbacks.beforeDrop(dragEventArgs)) + // promise resolved (or callback didn't return false) + .then(function (allowDrop) { + if (allowDrop !== false && scope.$$allowNodeDrop && !outOfBounds) { // node drop accepted) + dragInfo.apply(); + // fire the dropped callback only if the move was successful + scope.$treeScope.$callbacks.dropped(dragEventArgs); + } else { // drop canceled - revert the node to its original position + bindDragStartEvents(); + } + }) + // promise rejected - revert the node to its original position + .catch(function () { + bindDragStartEvents(); + }) + .finally(function () { + hiddenPlaceElm.replaceWith(scope.$element); + placeElm.remove(); + + if (dragElm) { // drag element is attached to the mouse pointer + dragElm.remove(); + dragElm = null; + } + scope.$treeScope.$callbacks.dragStop(dragEventArgs); + scope.$$allowNodeDrop = false; + dragInfo = null; + + // Restore cursor in Opera 12.16 and IE + var oldCur = document.body.getAttribute('ui-tree-cursor'); + if (oldCur !== null) { + $document.find('body').css({'cursor': oldCur}); + document.body.removeAttribute('ui-tree-cursor'); + } + }); + }); + }; + + dragStartEvent = function (e) { + if (scope.dragEnabled()) { + dragStart(e); + } + }; + + dragMoveEvent = function (e) { + dragMove(e); + }; + + dragEndEvent = function (e) { + scope.$$allowNodeDrop = true; + dragEnd(e); + }; + + dragCancelEvent = function (e) { + dragEnd(e); + }; + + dragDelay = (function () { + var to; + + return { + exec: function (fn, ms) { + if (!ms) { + ms = 0; + } + this.cancel(); + to = $timeout(fn, ms); + }, + cancel: function () { + $timeout.cancel(to); + } + }; + })(); + + /** + * Binds the mouse/touch events to enable drag start for this node + */ + bindDragStartEvents = function () { + element.bind('touchstart mousedown', function (e) { + dragDelay.exec(function () { + dragStartEvent(e); + }, scope.dragDelay || 0); + }); + element.bind('touchend touchcancel mouseup', function () { + dragDelay.cancel(); + }); + }; + bindDragStartEvents(); + + /** + * Binds mouse/touch events that handle moving/dropping this dragged node + */ + bindDragMoveEvents = function () { + angular.element($document).bind('touchend', dragEndEvent); + angular.element($document).bind('touchcancel', dragEndEvent); + angular.element($document).bind('touchmove', dragMoveEvent); + angular.element($document).bind('mouseup', dragEndEvent); + angular.element($document).bind('mousemove', dragMoveEvent); + angular.element($document).bind('mouseleave', dragCancelEvent); + }; + + /** + * Unbinds mouse/touch events that handle moving/dropping this dragged node + */ + unbindDragMoveEvents = function () { + angular.element($document).unbind('touchend', dragEndEvent); + angular.element($document).unbind('touchcancel', dragEndEvent); + angular.element($document).unbind('touchmove', dragMoveEvent); + angular.element($document).unbind('mouseup', dragEndEvent); + angular.element($document).unbind('mousemove', dragMoveEvent); + angular.element($document).unbind('mouseleave', dragCancelEvent); + }; + + keydownHandler = function (e) { + if (e.keyCode == 27) { + scope.$$allowNodeDrop = false; + dragEnd(e); + } + }; + + angular.element($window.document).bind('keydown', keydownHandler); + + //unbind handler that retains scope + scope.$on('$destroy', function () { + angular.element($window.document).unbind('keydown', keydownHandler); + }); + } + }; + } + ]); + +})(); + +(function () { + 'use strict'; + + angular.module('ui.tree') + .directive('uiTreeNodes', ['treeConfig', '$window', + function (treeConfig) { + return { + require: ['ngModel', '?^uiTreeNode', '^uiTree'], + restrict: 'A', + scope: true, + controller: 'TreeNodesController', + link: function (scope, element, attrs, controllersArr) { + + var config = {}, + ngModel = controllersArr[0], + treeNodeCtrl = controllersArr[1], + treeCtrl = controllersArr[2]; + + angular.extend(config, treeConfig); + if (config.nodesClass) { + element.addClass(config.nodesClass); + } + + if (treeNodeCtrl) { + treeNodeCtrl.scope.$childNodesScope = scope; + scope.$nodeScope = treeNodeCtrl.scope; + } else { + // find the root nodes if there is no parent node and have a parent ui-tree + treeCtrl.scope.$nodesScope = scope; + } + scope.$treeScope = treeCtrl.scope; + + if (ngModel) { + ngModel.$render = function () { + scope.$modelValue = ngModel.$modelValue; + }; + } + + scope.$watch(function () { + return attrs.maxDepth; + }, function (val) { + if ((typeof val) == 'number') { + scope.maxDepth = val; + } + }); + + scope.$watch(function () { + return attrs.nodropEnabled; + }, function (newVal) { + if ((typeof newVal) != 'undefined') { + scope.nodropEnabled = true; + } + }, true); + + attrs.$observe('horizontal', function (val) { + scope.horizontal = ((typeof val) != 'undefined'); + }); + + } + }; + } + ]); +})(); + +(function () { + 'use strict'; + + angular.module('ui.tree') + + /** + * @ngdoc service + * @name ui.tree.service:UiTreeHelper + * @requires ng.$document + * @requires ng.$window + * + * @description + * angular-ui-tree. + */ + .factory('UiTreeHelper', ['$document', '$window', 'treeConfig', + function ($document, $window, treeConfig) { + return { + + /** + * A hashtable used to storage data of nodes + * @type {Object} + */ + nodesData: {}, + + setNodeAttribute: function (scope, attrName, val) { + if (!scope.$modelValue) { + return null; + } + var data = this.nodesData[scope.$modelValue.$$hashKey]; + if (!data) { + data = {}; + this.nodesData[scope.$modelValue.$$hashKey] = data; + } + data[attrName] = val; + }, + + getNodeAttribute: function (scope, attrName) { + if (!scope.$modelValue) { + return null; + } + var data = this.nodesData[scope.$modelValue.$$hashKey]; + if (data) { + return data[attrName]; + } + return null; + }, + + /** + * @ngdoc method + * @methodOf ui.tree.service:$nodrag + * @param {Object} targetElm angular element + * @return {Bool} check if the node can be dragged. + */ + nodrag: function (targetElm) { + if (typeof targetElm.attr('data-nodrag') != 'undefined') { + return targetElm.attr('data-nodrag') !== 'false'; + } + return false; + }, + + /** + * get the event object for touches + * @param {[type]} e [description] + * @return {[type]} [description] + */ + eventObj: function (e) { + var obj = e; + if (e.targetTouches !== undefined) { + obj = e.targetTouches.item(0); + } else if (e.originalEvent !== undefined && e.originalEvent.targetTouches !== undefined) { + obj = e.originalEvent.targetTouches.item(0); + } + return obj; + }, + + dragInfo: function (node) { + return { + source: node, + sourceInfo: { + cloneModel: node.$treeScope.cloneEnabled === true ? angular.copy(node.$modelValue) : undefined, + nodeScope: node, + index: node.index(), + nodesScope: node.$parentNodesScope + }, + index: node.index(), + siblings: node.siblings().slice(0), + parent: node.$parentNodesScope, + + // Move the node to a new position + moveTo: function (parent, siblings, index) { + this.parent = parent; + this.siblings = siblings.slice(0); + + // If source node is in the target nodes + var i = this.siblings.indexOf(this.source); + if (i > -1) { + this.siblings.splice(i, 1); + if (this.source.index() < index) { + index--; + } + } + + this.siblings.splice(index, 0, this.source); + this.index = index; + }, + + parentNode: function () { + return this.parent.$nodeScope; + }, + + prev: function () { + if (this.index > 0) { + return this.siblings[this.index - 1]; + } + + return null; + }, + + next: function () { + if (this.index < this.siblings.length - 1) { + return this.siblings[this.index + 1]; + } + + return null; + }, + + isClone: function () { + return this.source.$treeScope.cloneEnabled === true; + }, + + clonedNode: function (node) { + return angular.copy(node); + }, + + isDirty: function () { + return this.source.$parentNodesScope != this.parent || + this.source.index() != this.index; + }, + + isForeign: function () { + return this.source.$treeScope !== this.parent.$treeScope; + }, + + eventArgs: function (elements, pos) { + return { + source: this.sourceInfo, + dest: { + index: this.index, + nodesScope: this.parent + }, + elements: elements, + pos: pos + }; + }, + + apply: function () { + + var nodeData = this.source.$modelValue; + + // nodrop enabled on tree or parent + if (this.parent.nodropEnabled || this.parent.$treeScope.nodropEnabled) { + return; + } + + // node was dropped in the same place - do nothing + if (!this.isDirty()) { + return; + } + + // cloneEnabled and cross-tree so copy and do not remove from source + if (this.isClone() && this.isForeign()) { + this.parent.insertNode(this.index, this.sourceInfo.cloneModel); + } else { // Any other case, remove and reinsert + this.source.remove(); + this.parent.insertNode(this.index, nodeData); + } + } + }; + }, + + /** + * @ngdoc method + * @name ui.tree#height + * @methodOf ui.tree.service:UiTreeHelper + * + * @description + * Get the height of an element. + * + * @param {Object} element Angular element. + * @returns {String} Height + */ + height: function (element) { + return element.prop('scrollHeight'); + }, + + /** + * @ngdoc method + * @name ui.tree#width + * @methodOf ui.tree.service:UiTreeHelper + * + * @description + * Get the width of an element. + * + * @param {Object} element Angular element. + * @returns {String} Width + */ + width: function (element) { + return element.prop('scrollWidth'); + }, + + /** + * @ngdoc method + * @name ui.tree#offset + * @methodOf ui.nestedSortable.service:UiTreeHelper + * + * @description + * Get the offset values of an element. + * + * @param {Object} element Angular element. + * @returns {Object} Object with properties width, height, top and left + */ + offset: function (element) { + var boundingClientRect = element[0].getBoundingClientRect(); + + return { + width: element.prop('offsetWidth'), + height: element.prop('offsetHeight'), + top: boundingClientRect.top + ($window.pageYOffset || $document[0].body.scrollTop || $document[0].documentElement.scrollTop), + left: boundingClientRect.left + ($window.pageXOffset || $document[0].body.scrollLeft || $document[0].documentElement.scrollLeft) + }; + }, + + /** + * @ngdoc method + * @name ui.tree#positionStarted + * @methodOf ui.tree.service:UiTreeHelper + * + * @description + * Get the start position of the target element according to the provided event properties. + * + * @param {Object} e Event + * @param {Object} target Target element + * @returns {Object} Object with properties offsetX, offsetY, startX, startY, nowX and dirX. + */ + positionStarted: function (e, target) { + var pos = {}, + pageX = e.pageX, + pageY = e.pageY; + + if (e.originalEvent && e.originalEvent.touches && (e.originalEvent.touches.length > 0)) { + pageX = e.originalEvent.touches[0].pageX; + pageY = e.originalEvent.touches[0].pageY; + } + pos.offsetX = pageX - this.offset(target).left; + pos.offsetY = pageY - this.offset(target).top; + pos.startX = pos.lastX = pageX; + pos.startY = pos.lastY = pageY; + pos.nowX = pos.nowY = pos.distX = pos.distY = pos.dirAx = 0; + pos.dirX = pos.dirY = pos.lastDirX = pos.lastDirY = pos.distAxX = pos.distAxY = 0; + return pos; + }, + + positionMoved: function (e, pos, firstMoving) { + var pageX = e.pageX, + pageY = e.pageY, + newAx; + if (e.originalEvent && e.originalEvent.touches && (e.originalEvent.touches.length > 0)) { + pageX = e.originalEvent.touches[0].pageX; + pageY = e.originalEvent.touches[0].pageY; + } + // mouse position last events + pos.lastX = pos.nowX; + pos.lastY = pos.nowY; + + // mouse position this events + pos.nowX = pageX; + pos.nowY = pageY; + + // distance mouse moved between events + pos.distX = pos.nowX - pos.lastX; + pos.distY = pos.nowY - pos.lastY; + + // direction mouse was moving + pos.lastDirX = pos.dirX; + pos.lastDirY = pos.dirY; + + // direction mouse is now moving (on both axis) + pos.dirX = pos.distX === 0 ? 0 : pos.distX > 0 ? 1 : -1; + pos.dirY = pos.distY === 0 ? 0 : pos.distY > 0 ? 1 : -1; + + // axis mouse is now moving on + newAx = Math.abs(pos.distX) > Math.abs(pos.distY) ? 1 : 0; + + // do nothing on first move + if (firstMoving) { + pos.dirAx = newAx; + pos.moving = true; + return; + } + + // calc distance moved on this axis (and direction) + if (pos.dirAx !== newAx) { + pos.distAxX = 0; + pos.distAxY = 0; + } else { + pos.distAxX += Math.abs(pos.distX); + if (pos.dirX !== 0 && pos.dirX !== pos.lastDirX) { + pos.distAxX = 0; + } + + pos.distAxY += Math.abs(pos.distY); + if (pos.dirY !== 0 && pos.dirY !== pos.lastDirY) { + pos.distAxY = 0; + } + } + + pos.dirAx = newAx; + }, + + elementIsTreeNode: function (element) { + return typeof element.attr('ui-tree-node') !== 'undefined'; + }, + + elementIsTreeNodeHandle: function (element) { + return typeof element.attr('ui-tree-handle') !== 'undefined'; + }, + elementIsTree: function (element) { + return typeof element.attr('ui-tree') !== 'undefined'; + }, + elementIsTreeNodes: function (element) { + return typeof element.attr('ui-tree-nodes') !== 'undefined'; + }, + elementIsPlaceholder: function (element) { + return element.hasClass(treeConfig.placeholderClass); + }, + elementContainsTreeNodeHandler: function (element) { + return element[0].querySelectorAll('[ui-tree-handle]').length >= 1; + }, + treeNodeHandlerContainerOfElement: function (element) { + return findFirstParentElementWithAttribute('ui-tree-handle', element[0]); + } + }; + } + ]); + + // TODO: optimize this loop + function findFirstParentElementWithAttribute(attributeName, childObj) { + // undefined if the mouse leaves the browser window + if (childObj === undefined) { + return null; + } + var testObj = childObj.parentNode, + count = 1, + // check for setAttribute due to exception thrown by Firefox when a node is dragged outside the browser window + res = (typeof testObj.setAttribute === 'function' && testObj.hasAttribute(attributeName)) ? testObj : null; + while (testObj && typeof testObj.setAttribute === 'function' && !testObj.hasAttribute(attributeName)) { + testObj = testObj.parentNode; + res = testObj; + if (testObj === document.documentElement) { + res = null; + break; + } + count++; + } + + return res; + } + +})(); + +/** + * @license AngularJS v1.5.10 + * (c) 2010-2016 Google, Inc. http://angularjs.org + * License: MIT + */ +(function(window) {'use strict'; + +/** + * @description + * + * This object provides a utility for producing rich Error messages within + * Angular. It can be called as follows: + * + * var exampleMinErr = minErr('example'); + * throw exampleMinErr('one', 'This {0} is {1}', foo, bar); + * + * The above creates an instance of minErr in the example namespace. The + * resulting error will have a namespaced error code of example.one. The + * resulting error will replace {0} with the value of foo, and {1} with the + * value of bar. The object is not restricted in the number of arguments it can + * take. + * + * If fewer arguments are specified than necessary for interpolation, the extra + * interpolation markers will be preserved in the final string. + * + * Since data will be parsed statically during a build step, some restrictions + * are applied with respect to how minErr instances are created and called. + * Instances should have names of the form namespaceMinErr for a minErr created + * using minErr('namespace') . Error codes, namespaces and template strings + * should all be static strings, not variables or general expressions. + * + * @param {string} module The namespace to use for the new minErr instance. + * @param {function} ErrorConstructor Custom error constructor to be instantiated when returning + * error from returned function, for cases when a particular type of error is useful. + * @returns {function(code:string, template:string, ...templateArgs): Error} minErr instance + */ + +function minErr(module, ErrorConstructor) { + ErrorConstructor = ErrorConstructor || Error; + return function() { + var SKIP_INDEXES = 2; + + var templateArgs = arguments, + code = templateArgs[0], + message = '[' + (module ? module + ':' : '') + code + '] ', + template = templateArgs[1], + paramPrefix, i; + + message += template.replace(/\{\d+\}/g, function(match) { + var index = +match.slice(1, -1), + shiftedIndex = index + SKIP_INDEXES; + + if (shiftedIndex < templateArgs.length) { + return toDebugString(templateArgs[shiftedIndex]); + } + + return match; + }); + + message += '\nhttp://errors.angularjs.org/1.5.10/' + + (module ? module + '/' : '') + code; + + for (i = SKIP_INDEXES, paramPrefix = '?'; i < templateArgs.length; i++, paramPrefix = '&') { + message += paramPrefix + 'p' + (i - SKIP_INDEXES) + '=' + + encodeURIComponent(toDebugString(templateArgs[i])); + } + + return new ErrorConstructor(message); + }; +} + +/* We need to tell ESLint what variables are being exported */ +/* exported + angular, + msie, + jqLite, + jQuery, + slice, + splice, + push, + toString, + ngMinErr, + angularModule, + uid, + REGEX_STRING_REGEXP, + VALIDITY_STATE_PROPERTY, + + lowercase, + uppercase, + manualLowercase, + manualUppercase, + nodeName_, + isArrayLike, + forEach, + forEachSorted, + reverseParams, + nextUid, + setHashKey, + extend, + toInt, + inherit, + merge, + noop, + identity, + valueFn, + isUndefined, + isDefined, + isObject, + isBlankObject, + isString, + isNumber, + isNumberNaN, + isDate, + isArray, + isFunction, + isRegExp, + isWindow, + isScope, + isFile, + isFormData, + isBlob, + isBoolean, + isPromiseLike, + trim, + escapeForRegexp, + isElement, + makeMap, + includes, + arrayRemove, + copy, + equals, + csp, + jq, + concat, + sliceArgs, + bind, + toJsonReplacer, + toJson, + fromJson, + convertTimezoneToLocal, + timezoneToOffset, + startingTag, + tryDecodeURIComponent, + parseKeyValue, + toKeyValue, + encodeUriSegment, + encodeUriQuery, + angularInit, + bootstrap, + getTestability, + snake_case, + bindJQuery, + assertArg, + assertArgFn, + assertNotHasOwnProperty, + getter, + getBlockNodes, + hasOwnProperty, + createMap, + + NODE_TYPE_ELEMENT, + NODE_TYPE_ATTRIBUTE, + NODE_TYPE_TEXT, + NODE_TYPE_COMMENT, + NODE_TYPE_DOCUMENT, + NODE_TYPE_DOCUMENT_FRAGMENT +*/ + +//////////////////////////////////// + +/** + * @ngdoc module + * @name ng + * @module ng + * @installation + * @description + * + * # ng (core module) + * The ng module is loaded by default when an AngularJS application is started. The module itself + * contains the essential components for an AngularJS application to function. The table below + * lists a high level breakdown of each of the services/factories, filters, directives and testing + * components available within this core module. + * + *
      + */ + +var REGEX_STRING_REGEXP = /^\/(.+)\/([a-z]*)$/; + +// The name of a form control's ValidityState property. +// This is used so that it's possible for internal tests to create mock ValidityStates. +var VALIDITY_STATE_PROPERTY = 'validity'; + +var hasOwnProperty = Object.prototype.hasOwnProperty; + +var lowercase = function(string) {return isString(string) ? string.toLowerCase() : string;}; +var uppercase = function(string) {return isString(string) ? string.toUpperCase() : string;}; + + +var manualLowercase = function(s) { + /* eslint-disable no-bitwise */ + return isString(s) + ? s.replace(/[A-Z]/g, function(ch) {return String.fromCharCode(ch.charCodeAt(0) | 32);}) + : s; + /* eslint-enable */ +}; +var manualUppercase = function(s) { + /* eslint-disable no-bitwise */ + return isString(s) + ? s.replace(/[a-z]/g, function(ch) {return String.fromCharCode(ch.charCodeAt(0) & ~32);}) + : s; + /* eslint-enable */ +}; + + +// String#toLowerCase and String#toUpperCase don't produce correct results in browsers with Turkish +// locale, for this reason we need to detect this case and redefine lowercase/uppercase methods +// with correct but slower alternatives. See https://github.com/angular/angular.js/issues/11387 +if ('i' !== 'I'.toLowerCase()) { + lowercase = manualLowercase; + uppercase = manualUppercase; +} + + +var + msie, // holds major version number for IE, or NaN if UA is not IE. + jqLite, // delay binding since jQuery could be loaded after us. + jQuery, // delay binding + slice = [].slice, + splice = [].splice, + push = [].push, + toString = Object.prototype.toString, + getPrototypeOf = Object.getPrototypeOf, + ngMinErr = minErr('ng'), + + /** @name angular */ + angular = window.angular || (window.angular = {}), + angularModule, + uid = 0; + +/** + * documentMode is an IE-only property + * http://msdn.microsoft.com/en-us/library/ie/cc196988(v=vs.85).aspx + */ +msie = window.document.documentMode; + + +/** + * @private + * @param {*} obj + * @return {boolean} Returns true if `obj` is an array or array-like object (NodeList, Arguments, + * String ...) + */ +function isArrayLike(obj) { + + // `null`, `undefined` and `window` are not array-like + if (obj == null || isWindow(obj)) return false; + + // arrays, strings and jQuery/jqLite objects are array like + // * jqLite is either the jQuery or jqLite constructor function + // * we have to check the existence of jqLite first as this method is called + // via the forEach method when constructing the jqLite object in the first place + if (isArray(obj) || isString(obj) || (jqLite && obj instanceof jqLite)) return true; + + // Support: iOS 8.2 (not reproducible in simulator) + // "length" in obj used to prevent JIT error (gh-11508) + var length = 'length' in Object(obj) && obj.length; + + // NodeList objects (with `item` method) and + // other objects with suitable length characteristics are array-like + return isNumber(length) && + (length >= 0 && ((length - 1) in obj || obj instanceof Array) || typeof obj.item === 'function'); + +} + +/** + * @ngdoc function + * @name angular.forEach + * @module ng + * @kind function + * + * @description + * Invokes the `iterator` function once for each item in `obj` collection, which can be either an + * object or an array. The `iterator` function is invoked with `iterator(value, key, obj)`, where `value` + * is the value of an object property or an array element, `key` is the object property key or + * array element index and obj is the `obj` itself. Specifying a `context` for the function is optional. + * + * It is worth noting that `.forEach` does not iterate over inherited properties because it filters + * using the `hasOwnProperty` method. + * + * Unlike ES262's + * [Array.prototype.forEach](http://www.ecma-international.org/ecma-262/5.1/#sec-15.4.4.18), + * providing 'undefined' or 'null' values for `obj` will not throw a TypeError, but rather just + * return the value provided. + * + ```js + var values = {name: 'misko', gender: 'male'}; + var log = []; + angular.forEach(values, function(value, key) { + this.push(key + ': ' + value); + }, log); + expect(log).toEqual(['name: misko', 'gender: male']); + ``` + * + * @param {Object|Array} obj Object to iterate over. + * @param {Function} iterator Iterator function. + * @param {Object=} context Object to become context (`this`) for the iterator function. + * @returns {Object|Array} Reference to `obj`. + */ + +function forEach(obj, iterator, context) { + var key, length; + if (obj) { + if (isFunction(obj)) { + for (key in obj) { + // Need to check if hasOwnProperty exists, + // as on IE8 the result of querySelectorAll is an object without a hasOwnProperty function + if (key !== 'prototype' && key !== 'length' && key !== 'name' && (!obj.hasOwnProperty || obj.hasOwnProperty(key))) { + iterator.call(context, obj[key], key, obj); + } + } + } else if (isArray(obj) || isArrayLike(obj)) { + var isPrimitive = typeof obj !== 'object'; + for (key = 0, length = obj.length; key < length; key++) { + if (isPrimitive || key in obj) { + iterator.call(context, obj[key], key, obj); + } + } + } else if (obj.forEach && obj.forEach !== forEach) { + obj.forEach(iterator, context, obj); + } else if (isBlankObject(obj)) { + // createMap() fast path --- Safe to avoid hasOwnProperty check because prototype chain is empty + for (key in obj) { + iterator.call(context, obj[key], key, obj); + } + } else if (typeof obj.hasOwnProperty === 'function') { + // Slow path for objects inheriting Object.prototype, hasOwnProperty check needed + for (key in obj) { + if (obj.hasOwnProperty(key)) { + iterator.call(context, obj[key], key, obj); + } + } + } else { + // Slow path for objects which do not have a method `hasOwnProperty` + for (key in obj) { + if (hasOwnProperty.call(obj, key)) { + iterator.call(context, obj[key], key, obj); + } + } + } + } + return obj; +} + +function forEachSorted(obj, iterator, context) { + var keys = Object.keys(obj).sort(); + for (var i = 0; i < keys.length; i++) { + iterator.call(context, obj[keys[i]], keys[i]); + } + return keys; +} + + +/** + * when using forEach the params are value, key, but it is often useful to have key, value. + * @param {function(string, *)} iteratorFn + * @returns {function(*, string)} + */ +function reverseParams(iteratorFn) { + return function(value, key) {iteratorFn(key, value);}; +} + +/** + * A consistent way of creating unique IDs in angular. + * + * Using simple numbers allows us to generate 28.6 million unique ids per second for 10 years before + * we hit number precision issues in JavaScript. + * + * Math.pow(2,53) / 60 / 60 / 24 / 365 / 10 = 28.6M + * + * @returns {number} an unique alpha-numeric string + */ +function nextUid() { + return ++uid; +} + + +/** + * Set or clear the hashkey for an object. + * @param obj object + * @param h the hashkey (!truthy to delete the hashkey) + */ +function setHashKey(obj, h) { + if (h) { + obj.$$hashKey = h; + } else { + delete obj.$$hashKey; + } +} + + +function baseExtend(dst, objs, deep) { + var h = dst.$$hashKey; + + for (var i = 0, ii = objs.length; i < ii; ++i) { + var obj = objs[i]; + if (!isObject(obj) && !isFunction(obj)) continue; + var keys = Object.keys(obj); + for (var j = 0, jj = keys.length; j < jj; j++) { + var key = keys[j]; + var src = obj[key]; + + if (deep && isObject(src)) { + if (isDate(src)) { + dst[key] = new Date(src.valueOf()); + } else if (isRegExp(src)) { + dst[key] = new RegExp(src); + } else if (src.nodeName) { + dst[key] = src.cloneNode(true); + } else if (isElement(src)) { + dst[key] = src.clone(); + } else { + if (!isObject(dst[key])) dst[key] = isArray(src) ? [] : {}; + baseExtend(dst[key], [src], true); + } + } else { + dst[key] = src; + } + } + } + + setHashKey(dst, h); + return dst; +} + +/** + * @ngdoc function + * @name angular.extend + * @module ng + * @kind function + * + * @description + * Extends the destination object `dst` by copying own enumerable properties from the `src` object(s) + * to `dst`. You can specify multiple `src` objects. If you want to preserve original objects, you can do so + * by passing an empty object as the target: `var object = angular.extend({}, object1, object2)`. + * + * **Note:** Keep in mind that `angular.extend` does not support recursive merge (deep copy). Use + * {@link angular.merge} for this. + * + * @param {Object} dst Destination object. + * @param {...Object} src Source object(s). + * @returns {Object} Reference to `dst`. + */ +function extend(dst) { + return baseExtend(dst, slice.call(arguments, 1), false); +} + + +/** +* @ngdoc function +* @name angular.merge +* @module ng +* @kind function +* +* @description +* Deeply extends the destination object `dst` by copying own enumerable properties from the `src` object(s) +* to `dst`. You can specify multiple `src` objects. If you want to preserve original objects, you can do so +* by passing an empty object as the target: `var object = angular.merge({}, object1, object2)`. +* +* Unlike {@link angular.extend extend()}, `merge()` recursively descends into object properties of source +* objects, performing a deep copy. +* +* @param {Object} dst Destination object. +* @param {...Object} src Source object(s). +* @returns {Object} Reference to `dst`. +*/ +function merge(dst) { + return baseExtend(dst, slice.call(arguments, 1), true); +} + + + +function toInt(str) { + return parseInt(str, 10); +} + +var isNumberNaN = Number.isNaN || function isNumberNaN(num) { + // eslint-disable-next-line no-self-compare + return num !== num; +}; + + +function inherit(parent, extra) { + return extend(Object.create(parent), extra); +} + +/** + * @ngdoc function + * @name angular.noop + * @module ng + * @kind function + * + * @description + * A function that performs no operations. This function can be useful when writing code in the + * functional style. + ```js + function foo(callback) { + var result = calculateResult(); + (callback || angular.noop)(result); + } + ``` + */ +function noop() {} +noop.$inject = []; + + +/** + * @ngdoc function + * @name angular.identity + * @module ng + * @kind function + * + * @description + * A function that returns its first argument. This function is useful when writing code in the + * functional style. + * + ```js + function transformer(transformationFn, value) { + return (transformationFn || angular.identity)(value); + }; + + // E.g. + function getResult(fn, input) { + return (fn || angular.identity)(input); + }; + + getResult(function(n) { return n * 2; }, 21); // returns 42 + getResult(null, 21); // returns 21 + getResult(undefined, 21); // returns 21 + ``` + * + * @param {*} value to be returned. + * @returns {*} the value passed in. + */ +function identity($) {return $;} +identity.$inject = []; + + +function valueFn(value) {return function valueRef() {return value;};} + +function hasCustomToString(obj) { + return isFunction(obj.toString) && obj.toString !== toString; +} + + +/** + * @ngdoc function + * @name angular.isUndefined + * @module ng + * @kind function + * + * @description + * Determines if a reference is undefined. + * + * @param {*} value Reference to check. + * @returns {boolean} True if `value` is undefined. + */ +function isUndefined(value) {return typeof value === 'undefined';} + + +/** + * @ngdoc function + * @name angular.isDefined + * @module ng + * @kind function + * + * @description + * Determines if a reference is defined. + * + * @param {*} value Reference to check. + * @returns {boolean} True if `value` is defined. + */ +function isDefined(value) {return typeof value !== 'undefined';} + + +/** + * @ngdoc function + * @name angular.isObject + * @module ng + * @kind function + * + * @description + * Determines if a reference is an `Object`. Unlike `typeof` in JavaScript, `null`s are not + * considered to be objects. Note that JavaScript arrays are objects. + * + * @param {*} value Reference to check. + * @returns {boolean} True if `value` is an `Object` but not `null`. + */ +function isObject(value) { + // http://jsperf.com/isobject4 + return value !== null && typeof value === 'object'; +} + + +/** + * Determine if a value is an object with a null prototype + * + * @returns {boolean} True if `value` is an `Object` with a null prototype + */ +function isBlankObject(value) { + return value !== null && typeof value === 'object' && !getPrototypeOf(value); +} + + +/** + * @ngdoc function + * @name angular.isString + * @module ng + * @kind function + * + * @description + * Determines if a reference is a `String`. + * + * @param {*} value Reference to check. + * @returns {boolean} True if `value` is a `String`. + */ +function isString(value) {return typeof value === 'string';} + + +/** + * @ngdoc function + * @name angular.isNumber + * @module ng + * @kind function + * + * @description + * Determines if a reference is a `Number`. + * + * This includes the "special" numbers `NaN`, `+Infinity` and `-Infinity`. + * + * If you wish to exclude these then you can use the native + * [`isFinite'](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/isFinite) + * method. + * + * @param {*} value Reference to check. + * @returns {boolean} True if `value` is a `Number`. + */ +function isNumber(value) {return typeof value === 'number';} + + +/** + * @ngdoc function + * @name angular.isDate + * @module ng + * @kind function + * + * @description + * Determines if a value is a date. + * + * @param {*} value Reference to check. + * @returns {boolean} True if `value` is a `Date`. + */ +function isDate(value) { + return toString.call(value) === '[object Date]'; +} + + +/** + * @ngdoc function + * @name angular.isArray + * @module ng + * @kind function + * + * @description + * Determines if a reference is an `Array`. Alias of Array.isArray. + * + * @param {*} value Reference to check. + * @returns {boolean} True if `value` is an `Array`. + */ +var isArray = Array.isArray; + +/** + * @ngdoc function + * @name angular.isFunction + * @module ng + * @kind function + * + * @description + * Determines if a reference is a `Function`. + * + * @param {*} value Reference to check. + * @returns {boolean} True if `value` is a `Function`. + */ +function isFunction(value) {return typeof value === 'function';} + + +/** + * Determines if a value is a regular expression object. + * + * @private + * @param {*} value Reference to check. + * @returns {boolean} True if `value` is a `RegExp`. + */ +function isRegExp(value) { + return toString.call(value) === '[object RegExp]'; +} + + +/** + * Checks if `obj` is a window object. + * + * @private + * @param {*} obj Object to check + * @returns {boolean} True if `obj` is a window obj. + */ +function isWindow(obj) { + return obj && obj.window === obj; +} + + +function isScope(obj) { + return obj && obj.$evalAsync && obj.$watch; +} + + +function isFile(obj) { + return toString.call(obj) === '[object File]'; +} + + +function isFormData(obj) { + return toString.call(obj) === '[object FormData]'; +} + + +function isBlob(obj) { + return toString.call(obj) === '[object Blob]'; +} + + +function isBoolean(value) { + return typeof value === 'boolean'; +} + + +function isPromiseLike(obj) { + return obj && isFunction(obj.then); +} + + +var TYPED_ARRAY_REGEXP = /^\[object (?:Uint8|Uint8Clamped|Uint16|Uint32|Int8|Int16|Int32|Float32|Float64)Array]$/; +function isTypedArray(value) { + return value && isNumber(value.length) && TYPED_ARRAY_REGEXP.test(toString.call(value)); +} + +function isArrayBuffer(obj) { + return toString.call(obj) === '[object ArrayBuffer]'; +} + + +var trim = function(value) { + return isString(value) ? value.trim() : value; +}; + +// Copied from: +// http://docs.closure-library.googlecode.com/git/local_closure_goog_string_string.js.source.html#line1021 +// Prereq: s is a string. +var escapeForRegexp = function(s) { + return s + .replace(/([-()[\]{}+?*.$^|,:#= 0) { + array.splice(index, 1); + } + return index; +} + +/** + * @ngdoc function + * @name angular.copy + * @module ng + * @kind function + * + * @description + * Creates a deep copy of `source`, which should be an object or an array. + * + * * If no destination is supplied, a copy of the object or array is created. + * * If a destination is provided, all of its elements (for arrays) or properties (for objects) + * are deleted and then all elements/properties from the source are copied to it. + * * If `source` is not an object or array (inc. `null` and `undefined`), `source` is returned. + * * If `source` is identical to `destination` an exception will be thrown. + * + *
      + *
      + * Only enumerable properties are taken into account. Non-enumerable properties (both on `source` + * and on `destination`) will be ignored. + *
      + * + * @param {*} source The source that will be used to make a copy. + * Can be any type, including primitives, `null`, and `undefined`. + * @param {(Object|Array)=} destination Destination into which the source is copied. If + * provided, must be of the same type as `source`. + * @returns {*} The copy or updated `destination`, if `destination` was specified. + * + * @example + + +
      +
      +
      +
      + Gender: +
      + + +
      +
      form = {{user | json}}
      +
      master = {{master | json}}
      +
      +
      + + // Module: copyExample + angular. + module('copyExample', []). + controller('ExampleController', ['$scope', function($scope) { + $scope.master = {}; + + $scope.reset = function() { + // Example with 1 argument + $scope.user = angular.copy($scope.master); + }; + + $scope.update = function(user) { + // Example with 2 arguments + angular.copy(user, $scope.master); + }; + + $scope.reset(); + }]); + +
      + */ +function copy(source, destination) { + var stackSource = []; + var stackDest = []; + + 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); + } + + return copyElement(source); + + function copyRecurse(source, destination) { + var h = destination.$$hashKey; + var key; + if (isArray(source)) { + for (var i = 0, ii = source.length; i < ii; i++) { + destination.push(copyElement(source[i])); + } + } 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]); + } + } 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]); + } + } + } else { + // Slowest path --- hasOwnProperty can't be called as a method + for (key in source) { + if (hasOwnProperty.call(source, key)) { + destination[key] = copyElement(source[key]); + } + } + } + setHashKey(destination, h); + return destination; + } + + function copyElement(source) { + // 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) + : 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); + } + } +} + + +/** + * @ngdoc function + * @name angular.equals + * @module ng + * @kind function + * + * @description + * Determines if two objects or two values are equivalent. Supports value types, regular + * expressions, arrays and objects. + * + * Two objects or values are considered equivalent if at least one of the following is true: + * + * * Both objects or values pass `===` comparison. + * * 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 JavaScript, + * /abc/ == /abc/ => false. But we consider two regular expressions as equal when their textual + * representation matches). + * + * During a property comparison, properties of `function` type and properties with names + * that begin with `$` are ignored. + * + * Scope and DOMWindow objects are being compared only by identify (`===`). + * + * @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 && t1 === 'object') { + if (isArray(o1)) { + if (!isArray(o2)) return false; + if ((length = o1.length) === o2.length) { + for (key = 0; key < length; key++) { + if (!equals(o1[key], o2[key])) return false; + } + return true; + } + } else if (isDate(o1)) { + if (!isDate(o2)) return false; + return equals(o1.getTime(), o2.getTime()); + } else if (isRegExp(o1)) { + if (!isRegExp(o2)) return false; + return o1.toString() === o2.toString(); + } else { + if (isScope(o1) || isScope(o2) || isWindow(o1) || isWindow(o2) || + isArray(o2) || isDate(o2) || isRegExp(o2)) return false; + keySet = createMap(); + for (key in o1) { + if (key.charAt(0) === '$' || isFunction(o1[key])) continue; + if (!equals(o1[key], o2[key])) return false; + keySet[key] = true; + } + for (key in o2) { + if (!(key in keySet) && + key.charAt(0) !== '$' && + isDefined(o2[key]) && + !isFunction(o2[key])) return false; + } + return true; + } + } + return false; +} + +var csp = function() { + if (!isDefined(csp.rules)) { + + + var ngCspElement = (window.document.querySelector('[ng-csp]') || + window.document.querySelector('[data-ng-csp]')); + + if (ngCspElement) { + var ngCspAttribute = ngCspElement.getAttribute('ng-csp') || + ngCspElement.getAttribute('data-ng-csp'); + csp.rules = { + noUnsafeEval: !ngCspAttribute || (ngCspAttribute.indexOf('no-unsafe-eval') !== -1), + noInlineStyle: !ngCspAttribute || (ngCspAttribute.indexOf('no-inline-style') !== -1) + }; + } else { + csp.rules = { + noUnsafeEval: noUnsafeEval(), + noInlineStyle: false + }; + } + } + + return csp.rules; + + function noUnsafeEval() { + try { + // eslint-disable-next-line no-new, no-new-func + new Function(''); + return false; + } catch (e) { + return true; + } + } +}; + +/** + * @ngdoc directive + * @module ng + * @name ngJq + * + * @element ANY + * @param {string=} ngJq the name of the library available under `window` + * to be used for angular.element + * @description + * Use this directive to force the angular.element library. This should be + * used to force either jqLite by leaving ng-jq blank or setting the name of + * the jquery variable under window (eg. jQuery). + * + * Since angular looks for this directive when it is loaded (doesn't wait for the + * DOMContentLoaded event), it must be placed on an element that comes before the script + * which loads angular. Also, only the first instance of `ng-jq` will be used and all + * others ignored. + * + * @example + * This example shows how to force jqLite using the `ngJq` directive to the `html` tag. + ```html + + + ... + ... + + ``` + * @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)); +} + +function sliceArgs(args, startIndex) { + return slice.call(args, startIndex || 0); +} + + +/** + * @ngdoc function + * @name angular.bind + * @module ng + * @kind function + * + * @description + * Returns a function which calls function `fn` bound to `self` (`self` becomes the `this` for + * `fn`). You can supply optional `args` that are prebound to the function. This feature is also + * known as [partial application](http://en.wikipedia.org/wiki/Partial_application), as + * distinguished from [function currying](http://en.wikipedia.org/wiki/Currying#Contrast_with_partial_function_application). + * + * @param {Object} self Context which `fn` should be evaluated in. + * @param {function()} fn Function to be bound. + * @param {...*} args Optional arguments to be prebound to the `fn` function call. + * @returns {function()} Function that wraps the `fn` with all the specified bindings. + */ +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, concat(curryArgs, arguments, 0)) + : fn.apply(self, curryArgs); + } + : 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). + return fn; + } +} + + +function toJsonReplacer(key, value) { + var val = value; + + if (typeof key === 'string' && key.charAt(0) === '$' && key.charAt(1) === '$') { + val = undefined; + } else if (isWindow(value)) { + val = '$WINDOW'; + } else if (value && window.document === value) { + val = '$DOCUMENT'; + } else if (isScope(value)) { + val = '$SCOPE'; + } + + return val; +} + + +/** + * @ngdoc function + * @name angular.toJson + * @module ng + * @kind function + * + * @description + * 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|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 (isUndefined(obj)) return undefined; + if (!isNumber(pretty)) { + pretty = pretty ? 2 : null; + } + return JSON.stringify(obj, toJsonReplacer, pretty); +} + + +/** + * @ngdoc function + * @name angular.fromJson + * @module ng + * @kind function + * + * @description + * Deserializes a JSON string. + * + * @param {string} json JSON string to deserialize. + * @returns {Object|Array|string|number} Deserialized JSON string. + */ +function fromJson(json) { + return isString(json) + ? JSON.parse(json) + : json; +} + + +var ALL_COLONS = /:/g; +function timezoneToOffset(timezone, fallback) { + // 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) { /* empty */ } + var elemHtml = jqLite('
      ').append(element).html(); + try { + 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); + } + +} + + +///////////////////////////////////////////////// + +/** + * Tries to decode the URI component without throwing an exception. + * + * @private + * @param str value potential URI component to check. + * @returns {boolean} True if `value` can be decoded + * with the decodeURIComponent function. + */ +function tryDecodeURIComponent(value) { + try { + return decodeURIComponent(value); + } catch (e) { + // Ignore any invalid uri component. + } +} + + +/** + * Parses an escaped url query string into key-value pairs. + * @returns {Object.} + */ +function parseKeyValue(/**string*/keyValue) { + 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])) { + obj[key].push(val); + } else { + obj[key] = [obj[key],val]; + } + } + } + }); + return obj; +} + +function toKeyValue(obj) { + var parts = []; + forEach(obj, function(value, key) { + if (isArray(value)) { + forEach(value, function(arrayValue) { + parts.push(encodeUriQuery(key, true) + + (arrayValue === true ? '' : '=' + encodeUriQuery(arrayValue, true))); + }); + } else { + parts.push(encodeUriQuery(key, true) + + (value === true ? '' : '=' + encodeUriQuery(value, true))); + } + }); + return parts.length ? parts.join('&') : ''; +} + + +/** + * We need our custom method because encodeURIComponent is too aggressive and doesn't follow + * http://www.ietf.org/rfc/rfc3986.txt with regards to the character set (pchar) allowed in path + * segments: + * segment = *pchar + * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" + * pct-encoded = "%" HEXDIG HEXDIG + * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" + * / "*" / "+" / "," / ";" / "=" + */ +function encodeUriSegment(val) { + return encodeUriQuery(val, true). + replace(/%26/gi, '&'). + replace(/%3D/gi, '='). + replace(/%2B/gi, '+'); +} + + +/** + * 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 / "/" / "?" ) + * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" + * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + * pct-encoded = "%" HEXDIG HEXDIG + * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" + * / "*" / "+" / "," / ";" / "=" + */ +function encodeUriQuery(val, pctEncodeSpaces) { + return encodeURIComponent(val). + 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) { + if (!document.currentScript) { + return true; + } + var src = document.currentScript.getAttribute('src'); + var link = document.createElement('a'); + link.href = src; + 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 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 + * + * Use this directive to **auto-bootstrap** an AngularJS application. The `ngApp` directive + * designates the **root element** of the application and is typically placed near the root element + * of the page - e.g. on the `` or `` tags. + * + * 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. 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. + * + * In the example below if the `ngApp` directive were not placed on the `html` element then the + * 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. + * + + +
      + I can add: {{a}} + {{b}} = {{ a+b }} +
      +
      + + angular.module('ngAppDemo', []).controller('ngAppDemoController', function($scope) { + $scope.a = 1; + $scope.b = 2; + }); + +
      + * + * 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 appElement, + module, + config = {}; + + // The element `element` has priority over any other element. + forEach(ngAttrPrefixes, function(prefix) { + var name = prefix + 'app'; + + if (!appElement && element.hasAttribute && element.hasAttribute(name)) { + appElement = element; + module = element.getAttribute(name); + } + }); + forEach(ngAttrPrefixes, function(prefix) { + var name = prefix + 'app'; + var candidate; + + if (!appElement && (candidate = element.querySelector('[' + name.replace(':', '\\:') + ']'))) { + appElement = candidate; + module = candidate.getAttribute(name); + } + }); + if (appElement) { + 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 `config` block. + * See: {@link angular.module modules} + * @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, 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] === 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, config.strictDi); + injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector', + function bootstrapApply(scope, element, compile, injector) { + scope.$apply(function() { + element.data('$injector', injector); + compile(element)(scope); + }); + }] + ); + 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(); + } + + window.name = window.name.replace(NG_DEFER_BOOTSTRAP, ''); + angular.resumeBootstrap = function(extraModules) { + forEach(extraModules, function(module) { + modules.push(module); + }); + 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) { + 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; + 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, + injector: JQLitePrototype.injector, + inheritedData: JQLitePrototype.inheritedData + }); + + // 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; +} + +/** + * throw error if the argument is falsy. + */ +function assertArg(arg, name, reason) { + if (!arg) { + throw ngMinErr('areq', 'Argument \'{0}\' is {1}', (name || '?'), (reason || 'required')); + } + return arg; +} + +function assertArgFn(arg, name, acceptArrayAnnotation) { + if (acceptArrayAnnotation && isArray(arg)) { + arg = arg[arg.length - 1]; + } + + assertArg(isFunction(arg), name, 'not a function, got ' + + (arg && typeof arg === 'object' ? arg.constructor.name || 'Object' : typeof arg)); + return arg; +} + +/** + * throw error if the name given is hasOwnProperty + * @param {String} name the name to test + * @param {String} context the context in which the name is used, such as module or directive + */ +function assertNotHasOwnProperty(name, context) { + if (name === 'hasOwnProperty') { + 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} [bindFnToScope=true] + * @returns {Object} value as accessible by path + */ +//TODO(misko): this function needs to be removed +function getter(obj, path, bindFnToScope) { + if (!path) return obj; + var keys = path.split('.'); + var key; + var lastInstance = obj; + var len = keys.length; + + for (var i = 0; i < len; i++) { + key = keys[i]; + if (obj) { + obj = (lastInstance = obj)[key]; + } + } + if (!bindFnToScope && isFunction(obj)) { + return bind(lastInstance, obj); + } + return obj; +} + +/** + * Return the DOM siblings between the first and last node in the given array. + * @param {Array} array like object + * @returns {Array} the inputted object or a jqLite collection containing the nodes + */ +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); + } + } + + return blockNodes || nodes; +} + + +/** + * 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); +} + +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}. + */ + +function setupModuleLoader(window) { + + var $injectorMinErr = minErr('$injector'); + var ngMinErr = minErr('ng'); + + function ensure(obj, name, factory) { + return obj[name] || (obj[name] = factory()); + } + + var angular = ensure(window, 'angular', Object); + + // We need to expose `angular.$$minErr` to modules such as `ngResource` that reference it during bootstrap + angular.$$minErr = angular.$$minErr || minErr; + + return ensure(angular, 'module', function() { + /** @type {Object.} */ + var modules = {}; + + /** + * @ngdoc function + * @name angular.module + * @module ng + * @description + * + * The `angular.module` is a global place for creating, registering and retrieving Angular + * modules. + * All modules (angular core or 3rd party) that should be available to an application must be + * registered using this mechanism. + * + * 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, 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', []); + * + * // register a new service + * myModule.value('appName', 'MyCoolApp'); + * + * // configure existing services inside initialization blocks. + * myModule.config(['$locationProvider', function($locationProvider) { + * // Configure existing providers + * $locationProvider.hashPrefix('!'); + * }]); + * ``` + * + * Then you can create an injector and load your modules like this: + * + * ```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 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 assertNotHasOwnProperty = function(name, context) { + if (name === 'hasOwnProperty') { + throw ngMinErr('badname', 'hasOwnProperty is not a valid {0} name', context); + } + }; + + assertNotHasOwnProperty(name, 'module'); + if (requires && modules.hasOwnProperty(name)) { + modules[name] = null; + } + 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); + } + + /** @type {!Array.>} */ + var invokeQueue = []; + + /** @type {!Array.} */ + var configBlocks = []; + + /** @type {!Array.} */ + var runBlocks = []; + + var config = invokeLater('$injector', 'invoke', 'push', configBlocks); + + /** @type {angular.Module} */ + var moduleInstance = { + // Private state + _invokeQueue: invokeQueue, + _configBlocks: configBlocks, + _runBlocks: runBlocks, + + /** + * @ngdoc property + * @name angular.Module#requires + * @module ng + * + * @description + * Holds the list of modules which the injector will load before the current module is + * loaded. + */ + requires: requires, + + /** + * @ngdoc property + * @name angular.Module#name + * @module ng + * + * @description + * Name of the module. + */ + name: name, + + + /** + * @ngdoc method + * @name angular.Module#provider + * @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()}. + */ + provider: invokeLaterAndSetModuleName('$provide', 'provider'), + + /** + * @ngdoc method + * @name angular.Module#factory + * @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()}. + */ + factory: invokeLaterAndSetModuleName('$provide', 'factory'), + + /** + * @ngdoc method + * @name angular.Module#service + * @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()}. + */ + service: invokeLaterAndSetModuleName('$provide', 'service'), + + /** + * @ngdoc method + * @name angular.Module#value + * @module ng + * @param {string} name service name + * @param {*} object Service instance object. + * @description + * See {@link auto.$provide#value $provide.value()}. + */ + value: invokeLater('$provide', 'value'), + + /** + * @ngdoc method + * @name angular.Module#constant + * @module ng + * @param {string} name constant name + * @param {*} object Constant value. + * @description + * 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'), + + /** + * @ngdoc method + * @name angular.Module#animation + * @module ng + * @param {string} name animation name + * @param {Function} animationFactory Factory function for creating new instance of an + * animation. + * @description + * + * **NOTE**: animations take effect only if the **ngAnimate** module is loaded. + * + * + * Defines an animation hook that can be later used with + * {@link $animate $animate} service and directives that use this service. + * + * ```js + * module.animation('.animation-name', function($inject1, $inject2) { + * return { + * eventName : function(element, done) { + * //code to run the animation + * //once complete, then run done() + * return function cancellationFunction(element) { + * //code to cancel the animation + * } + * } + * } + * }) + * ``` + * + * See {@link ng.$animateProvider#register $animateProvider.register()} and + * {@link ngAnimate ngAnimate module} for more information. + */ + animation: invokeLaterAndSetModuleName('$animateProvider', 'register'), + + /** + * @ngdoc method + * @name angular.Module#filter + * @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: invokeLaterAndSetModuleName('$filterProvider', 'register'), + + /** + * @ngdoc method + * @name angular.Module#controller + * @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: invokeLaterAndSetModuleName('$controllerProvider', 'register'), + + /** + * @ngdoc method + * @name angular.Module#directive + * @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#directive $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 + * @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 + * @module ng + * @param {Function} initializationFn Execute this function after injector creation. + * Useful for application initialization. + * @description + * Use this method to register work which should be performed when the injector is done + * loading all modules. + */ + run: function(block) { + runBlocks.push(block); + return this; + } + }; + + if (configFn) { + config(configFn); + } + + return moduleInstance; + + /** + * @param {string} provider + * @param {string} method + * @param {String=} insertMethod + * @returns {angular.Module} + */ + function invokeLater(provider, method, insertMethod, queue) { + if (!queue) queue = invokeQueue; + return function() { + queue[insertMethod || 'push']([provider, method, arguments]); + return moduleInstance; + }; + } + + /** + * @param {string} provider + * @param {string} method + * @returns {angular.Module} + */ + function invokeLaterAndSetModuleName(provider, method) { + return function(recipeName, factoryFunction) { + if (factoryFunction && isFunction(factoryFunction)) factoryFunction.$$moduleName = name; + invokeQueue.push([provider, method, arguments]); + return moduleInstance; + }; + } + }); + }; + }); + +} + +/* global shallowCopy: true */ + +/** + * 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; +} + +/* global toDebugString: true */ + +function serializeObject(obj) { + var seen = []; + + 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) { + if (typeof obj === 'function') { + return obj.toString().replace(/ \{[\s\S]*$/, ''); + } else if (isUndefined(obj)) { + return 'undefined'; + } else if (typeof obj !== 'string') { + return serializeObject(obj); + } + 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, + $ExceptionHandlerProvider, + $FilterProvider, + $$ForceReflowProvider, + $InterpolateProvider, + $IntervalProvider, + $$HashMapProvider, + $HttpProvider, + $HttpParamSerializerProvider, + $HttpParamSerializerJQLikeProvider, + $HttpBackendProvider, + $xhrFactoryProvider, + $jsonpCallbacksProvider, + $LocationProvider, + $LogProvider, + $ParseProvider, + $RootScopeProvider, + $QProvider, + $$QProvider, + $$SanitizeUriProvider, + $SceProvider, + $SceDelegateProvider, + $SnifferProvider, + $TemplateCacheProvider, + $TemplateRequestProvider, + $$TestabilityProvider, + $TimeoutProvider, + $$RAFProvider, + $WindowProvider, + $$jqLiteProvider, + $$CookieReaderProvider +*/ + + +/** + * @ngdoc object + * @name angular.version + * @module ng + * @description + * 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". + * - `minor` – `{number}` – Minor version number, such as "9". + * - `dot` – `{number}` – Dot version number, such as "18". + * - `codeName` – `{string}` – Code name of the release, such as "jiggling-armfat". + */ +var version = { + // These placeholder strings will be replaced by grunt's `build` task. + // They need to be double- or single-quoted. + full: '1.5.10', + major: 1, + minor: 5, + dot: 10, + codeName: 'asynchronous-synchronization' +}; + + +function publishExternalAPI(angular) { + extend(angular, { + 'bootstrap': bootstrap, + 'copy': copy, + 'extend': extend, + 'merge': merge, + 'equals': equals, + 'element': jqLite, + 'forEach': forEach, + 'injector': createInjector, + 'noop': noop, + 'bind': bind, + 'toJson': toJson, + 'fromJson': fromJson, + 'identity': identity, + 'isUndefined': isUndefined, + 'isDefined': isDefined, + 'isString': isString, + 'isFunction': isFunction, + 'isObject': isObject, + 'isNumber': isNumber, + 'isElement': isElement, + 'isArray': isArray, + 'version': version, + 'isDate': isDate, + 'lowercase': lowercase, + 'uppercase': uppercase, + 'callbacks': {$$counter: 0}, + 'getTestability': getTestability, + '$$minErr': minErr, + '$$csp': csp, + 'reloadWithDebugInfo': reloadWithDebugInfo + }); + + angularModule = setupModuleLoader(window); + + angularModule('ng', ['ngLocale'], ['$provide', + function ngModule($provide) { + // $$sanitizeUriProvider needs to be before $compileProvider as it is used by it. + $provide.provider({ + $$sanitizeUri: $$SanitizeUriProvider + }); + $provide.provider('$compile', $CompileProvider). + 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, + $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, + $$rAF: $$RAFProvider, + $$jqLite: $$jqLiteProvider, + $$HashMap: $$HashMapProvider, + $$cookieReader: $$CookieReaderProvider + }); + } + ]); +} + +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * 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? * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +/* global JQLitePrototype: true, + addEventListenerFn: true, + removeEventListenerFn: true, + BOOLEAN_ATTR: true, + ALIASED_ATTR: true +*/ + +////////////////////////////////// +//JQLite +////////////////////////////////// + +/** + * @ngdoc function + * @name angular.element + * @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**. + * + * 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 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 (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/) - 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/) - Does not support functions as parameters + * - [`bind()`](http://api.jquery.com/bind/) - 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/) - 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 + * - [`hasClass()`](http://api.jquery.com/hasClass/) + * - [`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, 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/) + * - [`remove()`](http://api.jquery.com/remove/) + * - [`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/) - 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/) - Does not support namespaces or event object as parameter + * - [`val()`](http://api.jquery.com/val/) + * - [`wrap()`](http://api.jquery.com/wrap/) + * + * ## jQuery/jqLite Extras + * Angular also provides the following additional methods and events to both jQuery and jqLite: + * + * ### Events + * - `$destroy` - AngularJS intercepts all jqLite/jQuery's DOM destruction apis and fires this event + * on all DOM nodes being removed. This can be used to clean up any 3rd party bindings to the DOM + * element before it is removed. + * + * ### Methods + * - `controller(name)` - retrieves the controller of the current element or its parent. By default + * retrieves controller associated with the `ngController` directive. If `name` is provided as + * 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 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 = {}, + jqId = 1, + addEventListenerFn = function(element, type, fn) { + element.addEventListener(type, fn, false); + }, + removeEventListenerFn = function(element, type, fn) { + element.removeEventListener(type, fn, false); + }; + +/* + * !!! 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 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. + * @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'); +} + +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; + +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 jqLiteCleanData(nodes) { + for (var i = 0, ii = nodes.length; i < ii; i++) { + jqLiteRemoveData(nodes[i]); + } +} + +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 (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 (argIsString) { + jqLiteAddNodes(this, jqLiteParseHTML(element)); + } else { + jqLiteAddNodes(this, element); + } +} + +function jqLiteClone(element) { + return element.cloneNode(true); +} + +function jqLiteDealoc(element, onlyDescendants) { + if (!onlyDescendants) jqLiteRemoveData(element); + + if (element.querySelectorAll) { + var descendants = element.querySelectorAll('*'); + for (var i = 0, l = descendants.length; i < l; i++) { + jqLiteRemoveData(descendants[i]); + } + } +} + +function jqLiteOff(element, type, fn, unsupported) { + if (isDefined(unsupported)) throw jqLiteMinErr('offargs', 'jqLite#off() does not support the `selector` argument'); + + var expandoStore = jqLiteExpandoStore(element); + var events = expandoStore && expandoStore.events; + var handle = expandoStore && expandoStore.handle; + + if (!handle) return; //no listeners registered + + if (!type) { + for (type in events) { + if (type !== '$destroy') { + removeEventListenerFn(element, type, handle); + } + delete events[type]; + } + } else { + + var removeHandler = function(type) { + var listenerFns = events[type]; + if (isDefined(fn)) { + arrayRemove(listenerFns || [], fn); + } + if (!(isDefined(fn) && listenerFns && listenerFns.length > 0)) { + removeEventListenerFn(element, type, handle); + delete events[type]; + } + }; + + forEach(type.split(' '), function(type) { + removeHandler(type); + if (MOUSE_EVENT_MAP[type]) { + removeHandler(MOUSE_EVENT_MAP[type]); + } + }); + } +} + +function jqLiteRemoveData(element, name) { + var expandoId = element.ng339; + var expandoStore = expandoId && jqCache[expandoId]; + + if (expandoStore) { + if (name) { + delete expandoStore.data[name]; + return; + } + + if (expandoStore.handle) { + if (expandoStore.events.$destroy) { + expandoStore.handle({}, '$destroy'); + } + jqLiteOff(element); + } + delete jqCache[expandoId]; + element.ng339 = undefined; // don't delete DOM expandos. IE and Chrome don't like it + } +} + + +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) { + if (jqLiteAcceptsData(element)) { + + var isSimpleSetter = isDefined(value); + var isSimpleGetter = !isSimpleSetter && key && !isObject(key); + var massGetter = !key; + var expandoStore = jqLiteExpandoStore(element, !isSimpleGetter); + var data = expandoStore && expandoStore.data; + + if (isSimpleSetter) { // data('key', value) + data[key] = value; + } else { + 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[key]; + } else { // mass-setter: data({key1: val1, key2: val2}) + extend(data, key); + } + } + } + } +} + +function jqLiteHasClass(element, selector) { + if (!element.getAttribute) return false; + 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) + ' ', ' ')) + ); + }); + } +} + +function jqLiteAddClass(element, cssClasses) { + if (cssClasses && element.setAttribute) { + var existingClasses = (' ' + (element.getAttribute('class') || '') + ' ') + .replace(/[\n\t]/g, ' '); + + forEach(cssClasses.split(' '), function(cssClass) { + cssClass = trim(cssClass); + if (existingClasses.indexOf(' ' + cssClass + ' ') === -1) { + existingClasses += cssClass + ' '; + } + }); + + element.setAttribute('class', trim(existingClasses)); + } +} + + +function jqLiteAddNodes(root, elements) { + // THIS CODE IS VERY HOT. Don't make changes without benchmarking. + + if (elements) { + + // 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'); +} + +function jqLiteInheritedData(element, name, value) { + // if element is the document object work with the html element instead + // this makes $(document).scope() possible + if (element.nodeType === NODE_TYPE_DOCUMENT) { + element = element.documentElement; + } + var names = isArray(name) ? name : [name]; + + while (element) { + for (var i = 0, ii = names.length; i < ii; i++) { + if (isDefined(value = jqLite.data(element, names[i]))) return value; + } + + // 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) { + 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); + } +} + +////////////////////////////////////////// +// 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 is already loaded + if (window.document.readyState === 'complete') { + window.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. + // eslint-disable-next-line new-cap + JQLite(window).on('load', trigger); // fallback to window.onload for others + } + }, + toString: function() { + var value = []; + forEach(this, function(e) { value.push('' + e);}); + return '[' + value.join(', ') + ']'; + }, + + eq: function(index) { + return (index >= 0) ? jqLite(this[index]) : jqLite(this[this.length + index]); + }, + + length: 0, + push: push, + sort: [].sort, + splice: [].splice +}; + +////////////////////////////////////////// +// Functions iterating getter/setters. +// these functions return self on setter and +// value on get. +////////////////////////////////////////// +var BOOLEAN_ATTR = {}; +forEach('multiple,selected,checked,disabled,readOnly,required,open'.split(','), function(value) { + BOOLEAN_ATTR[lowercase(value)] = value; +}); +var BOOLEAN_ELEMENTS = {}; +forEach('input,select,option,textarea,button,form,details'.split(','), function(value) { + BOOLEAN_ELEMENTS[value] = true; +}); +var ALIASED_ATTR = { + 'ngMinlength': 'minlength', + 'ngMaxlength': 'maxlength', + 'ngMin': 'min', + 'ngMax': 'max', + 'ngPattern': 'pattern' +}; + +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[nodeName_(element)] && booleanAttr; +} + +function getAliasedAttrName(name) { + return ALIASED_ATTR[name]; +} + +forEach({ + data: jqLiteData, + removeData: jqLiteRemoveData, + hasData: jqLiteHasData, + cleanData: jqLiteCleanData +}, 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.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.data(element, '$isolateScope') || jqLite.data(element, '$isolateScopeNoTemplate'); + }, + + controller: jqLiteController, + + injector: function(element) { + return jqLiteInheritedData(element, '$injector'); + }, + + removeAttr: function(element, name) { + element.removeAttribute(name); + }, + + hasClass: jqLiteHasClass, + + css: function(element, name, value) { + name = camelCase(name); + + if (isDefined(value)) { + element.style[name] = value; + } else { + return element.style[name]; + } + }, + + attr: function(element, name, value) { + var nodeType = element.nodeType; + if (nodeType === NODE_TYPE_TEXT || nodeType === NODE_TYPE_ATTRIBUTE || nodeType === NODE_TYPE_COMMENT) { + 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); + } + } else { + return (element[name] || + (element.attributes.getNamedItem(name) || noop).specified) + ? lowercasedName + : undefined; + } + } 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) + return ret === null ? undefined : ret; + } + }, + + prop: function(element, name, value) { + if (isDefined(value)) { + element[name] = value; + } else { + return element[name]; + } + }, + + text: (function() { + getText.$dv = ''; + return getText; + + function getText(element, value) { + if (isUndefined(value)) { + var nodeType = element.nodeType; + return (nodeType === NODE_TYPE_ELEMENT || nodeType === NODE_TYPE_TEXT) ? element.textContent : ''; + } + element.textContent = value; + } + })(), + + val: function(element, value) { + if (isUndefined(value)) { + if (element.multiple && nodeName_(element) === 'select') { + var result = []; + forEach(element.options, function(option) { + if (option.selected) { + result.push(option.value || option.text); + } + }); + return result.length === 0 ? null : result; + } + return element.value; + } + element.value = value; + }, + + html: function(element, value) { + if (isUndefined(value)) { + return element.innerHTML; + } + jqLiteDealoc(element, true); + element.innerHTML = value; + }, + + empty: jqLiteEmpty +}, 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 && + (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 < nodeCount; i++) { + if (fn === jqLiteData) { + // data() takes the whole object in jQuery + fn(this[i], arg1); + } else { + for (key in arg1) { + fn(this[i], key, arg1[key]); + } + } + } + // return self for chaining + 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 = (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; + } + return value; + } + } else { + // we are a write, so apply to all children + for (i = 0; i < nodeCount; i++) { + fn(this[i], arg1, arg2); + } + // return self for chaining + return this; + } + }; +}); + +function createEventHandler(element, events) { + var eventHandler = function(event, type) { + // jQuery specific api + event.isDefaultPrevented = function() { + 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. + if ((eventFnsLength > 1)) { + eventFns = shallowCopy(eventFns); + } + + 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 +// selector. +////////////////////////////////////////// +forEach({ + removeData: jqLiteRemoveData, + + on: function jqLiteOn(element, type, fn, unsupported) { + if (isDefined(unsupported)) throw jqLiteMinErr('onargs', 'jqLite#on() does not support the `selector` or `eventData` parameters'); + + // Do not add event handlers to non-elements because they will not be cleaned up. + if (!jqLiteAcceptsData(element)) { + return; + } + + var expandoStore = jqLiteExpandoStore(element, true); + var events = expandoStore.events; + var handle = expandoStore.handle; + + 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) { + eventFns = events[type] = []; + eventFns.specialHandlerWrapper = specialHandlerWrapper; + if (type !== '$destroy' && !noEventListener) { + addEventListenerFn(element, type, handle); + } + } + + 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, + + one: function(element, type, fn) { + element = jqLite(element); + + //add the listener twice so that when it is called + //you can remove the original function and still be + //able to call element.off(ev, fn) normally + element.on(type, function onFn() { + element.off(type, fn); + element.off(type, onFn); + }); + element.on(type, fn); + }, + + replaceWith: function(element, replaceNode) { + var index, parent = element.parentNode; + jqLiteDealoc(element); + forEach(new JQLite(replaceNode), function(node) { + if (index) { + parent.insertBefore(node, index.nextSibling); + } else { + parent.replaceChild(node, element); + } + index = node; + }); + }, + + children: function(element) { + var children = []; + forEach(element.childNodes, function(element) { + if (element.nodeType === NODE_TYPE_ELEMENT) { + children.push(element); + } + }); + return children; + }, + + contents: function(element) { + return element.contentDocument || element.childNodes || []; + }, + + append: function(element, node) { + 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 === NODE_TYPE_ELEMENT) { + var index = element.firstChild; + forEach(new JQLite(node), function(child) { + element.insertBefore(child, index); + }); + } + }, + + wrap: function(element, wrapNode) { + jqLiteWrapNode(element, jqLite(wrapNode).eq(0).clone()[0]); + }, + + remove: jqLiteRemove, + + detach: function(element) { + jqLiteRemove(element, true); + }, + + after: function(element, newElement) { + var index = element, parent = element.parentNode; + + 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 (selector) { + forEach(selector.split(' '), function(className) { + var classCondition = condition; + if (isUndefined(classCondition)) { + classCondition = !jqLiteHasClass(element, className); + } + (classCondition ? jqLiteAddClass : jqLiteRemoveClass)(element, className); + }); + } + }, + + parent: function(element) { + var parent = element.parentNode; + return parent && parent.nodeType !== NODE_TYPE_DOCUMENT_FRAGMENT ? parent : null; + }, + + next: function(element) { + return element.nextElementSibling; + }, + + find: function(element, selector) { + if (element.getElementsByTagName) { + return element.getElementsByTagName(selector); + } else { + return []; + } + }, + + clone: jqLiteClone, + + triggerHandler: function(element, event, extraParameters) { + + var dummyEvent, eventFnsCopy, handlerArgs; + var eventName = event.type || event; + var expandoStore = jqLiteExpandoStore(element); + var events = expandoStore && expandoStore.events; + var eventFns = events && events[eventName]; + + 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 + }; + + // 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) { + /** + * chaining functions + */ + JQLite.prototype[name] = function(arg1, arg2, arg3) { + var value; + + for (var i = 0, ii = this.length; i < ii; i++) { + if (isUndefined(value)) { + value = fn(this[i], arg1, arg2, arg3); + if (isDefined(value)) { + // any function which returns a value needs to be wrapped + value = jqLite(value); + } + } else { + jqLiteAddNodes(value, fn(this[i], arg1, arg2, arg3)); + } + } + return isDefined(value) ? value : this; + }; +}); + +// 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: + * string is string + * number is number as string + * object is either result of calling $$hashKey function on the object or uniquely generated id, + * that is also assigned to the $$hashKey property of the object. + * + * @param obj + * @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, nextUidFn) { + var key = obj && obj.$$hashKey; + + if (key) { + if (typeof key === 'function') { + key = obj.$$hashKey(); + } + return 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, isolatedUid) { + if (isolatedUid) { + var uid = 0; + this.nextUid = function() { + return ++uid; + }; + } + forEach(array, this.put, this); +} +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, this.nextUid)] = value; + }, + + /** + * @param key + * @returns {Object} the value for the key + */ + get: function(key) { + return this[hashKey(key, this.nextUid)]; + }, + + /** + * Remove the key/value pair + * @param key + */ + remove: function(key) { + var value = this[key = hashKey(key, this.nextUid)]; + delete this[key]; + return value; + } +}; + +var $$HashMapProvider = [/** @this */function() { + this.$get = [function() { + return HashMap; + }]; +}]; + +/** + * @ngdoc function + * @module ng + * @name angular.injector + * @kind function + * + * @description + * 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. + * @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) { + * $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 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 + * markup.* + * + * In the following example a new block of HTML containing a `ng-controller` + * 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); + * + * angular.element(document).injector().invoke(function($compile) { + * var scope = angular.element($div).scope(); + * $compile($div)(scope); + * }); + * ``` + */ + + +/** + * @ngdoc module + * @name auto + * @installation + * @description + * + * Implicit module which gets automatically added to each {@link auto.$injector $injector}. + */ + +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 stringifyFn(fn) { + // Support: Chrome 50-51 only + // Creating a new string by adding `' '` at the end, to hack around some bug in Chrome v50/51 + // (See https://github.com/angular/angular.js/issues/14487.) + // TODO (gkalpak): Remove workaround when Chrome v52 is released + 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) { + 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); + }); + }); + } + fn.$inject = $inject; + } + } else if (isArray(fn)) { + last = fn.length - 1; + assertArgFn(fn[last], 'fn'); + $inject = fn.slice(0, last); + } else { + assertArgFn(fn, 'fn', true); + } + return $inject; +} + +/////////////////////////////////////// + +/** + * @ngdoc service + * @name $injector + * + * @description + * + * `$injector` is used to retrieve object instances as defined by + * {@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) { + * return $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){}); + * + * // annotated + * function explicit(serviceA) {}; + * explicit.$inject = ['serviceA']; + * $injector.invoke(explicit); + * + * // 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. 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 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 method + * @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 $injector#invoke + * + * @description + * Invoke the method and supply the method arguments from the `$injector`. + * + * @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. + * @returns {*} the value returned by the invoked `fn` function. + */ + +/** + * @ngdoc method + * @name $injector#has + * + * @description + * Allows the user to query if the particular service exists. + * + * @param {string} name Name of the service to query. + * @returns {boolean} `true` if injector has given service. + */ + +/** + * @ngdoc method + * @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 + * constructor annotation. + * + * @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`. + */ + +/** + * @ngdoc method + * @name $injector#annotate + * + * @description + * Returns an array of service names which the function is requesting for injection. This API is + * used by the injector to determine which services need to be injected into the function when the + * function is invoked. There are three ways in which the function can be annotated with the needed + * dependencies. + * + * # Argument names + * + * 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) { + * // ... + * } + * + * // 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. + * + * # The `$inject` property + * + * 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) { + * // ... + * } + * // Define function dependencies + * MyController['$inject'] = ['$scope', '$route']; + * + * // Then + * expect(injector.annotate(MyController)).toEqual(['$scope', '$route']); + * ``` + * + * # The array notation + * + * It is often desirable to inline Injected functions and that's when setting the `$inject` property + * 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) { + * // ... + * }); + * + * // We are forced to write break inlining + * var tmpFn = function(obfuscatedCompile, obfuscatedRootScope) { + * // ... + * }; + * tmpFn.$inject = ['$compile', '$rootScope']; + * injector.invoke(tmpFn); + * + * // To better support inline function the inline annotation is supported + * injector.invoke(['$compile', '$rootScope', function(obfCompile, obfRootScope) { + * // ... + * }]); + * + * // Therefore + * 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 + * 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 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 + * {@link angular.Module}. + * + * An Angular **service** is a singleton object created by a **service factory**. These **service + * factories** are functions which, in turn, are created by a **service provider**. + * 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 + * 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 + * services without specifying a provider. + * + * * {@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#value value(name, obj)} - registers a value/object that can only be accessed by + * services, not providers. + * * {@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#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 $provide#provider + * @description + * + * 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. + * + * Service provider names start with the name of the service they provide followed by `Provider`. + * For example, the {@link ng.$log $log} service has a provider called + * {@link ng.$logProvider $logProvider}. + * + * Service provider objects can have additional methods which allow configuration of the provider + * and its service. Importantly, you can configure what kind of service is created by the `$get` + * method, or how that service will act. For example, the {@link ng.$logProvider $logProvider} has a + * method {@link ng.$logProvider#debugEnabled debugEnabled} + * which lets you specify whether the {@link ng.$log $log} service will log debug messages to the + * console or not. + * + * @param {string} name The name of the instance. NOTE: the provider will be available under `name + + 'Provider'` key. + * @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. + * - `Constructor`: a new instance of the provider will be created using + * {@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#provider $provide.provider()}. + * + * ```js + * // Define the eventTracker provider + * function EventTrackerProvider() { + * var trackingUrl = '/track'; + * + * // A provider method for configuring where the tracked events should been saved + * this.setTrackingUrl = function(url) { + * trackingUrl = url; + * }; + * + * // The service factory function + * this.$get = ['$http', function($http) { + * var trackedEvents = {}; + * return { + * // Call this to track an event + * event: function(event) { + * var count = trackedEvents[event] || 0; + * count += 1; + * trackedEvents[event] = count; + * return count; + * }, + * // Call this to save the tracked events to the trackingUrl + * save: function() { + * $http.post(trackingUrl, trackedEvents); + * } + * }; + * }]; + * } + * + * describe('eventTracker', function() { + * var postSpy; + * + * beforeEach(module(function($provide) { + * // Register the eventTracker provider + * $provide.provider('eventTracker', EventTrackerProvider); + * })); + * + * beforeEach(module(function(eventTrackerProvider) { + * // Configure eventTracker provider + * eventTrackerProvider.setTrackingUrl('/custom-track'); + * })); + * + * it('tracks events', inject(function(eventTracker) { + * expect(eventTracker.event('login')).toEqual(1); + * expect(eventTracker.event('login')).toEqual(2); + * })); + * + * it('saves to the tracking url', inject(function(eventTracker, $http) { + * postSpy = spyOn($http, 'post'); + * eventTracker.event('login'); + * eventTracker.save(); + * expect(postSpy).toHaveBeenCalled(); + * expect(postSpy.mostRecentCall.args[0]).not.toEqual('/track'); + * expect(postSpy.mostRecentCall.args[0]).toEqual('/custom-track'); + * expect(postSpy.mostRecentCall.args[1]).toEqual({ 'login': 1 }); + * })); + * }); + * ``` + */ + +/** + * @ngdoc method + * @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 + * configure your service in a provider. + * + * @param {string} name The name of the instance. + * @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 $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 a factory + * function that returns an instance instantiated by the injector from the service constructor + * function. + * + * 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|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#service $provide.service(class)}. + * ```js + * var Ping = function($http) { + * this.$http = $http; + * }; + * + * 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: + * ```js + * someModule.controller('Ctrl', ['ping', function(ping) { + * ping.send(); + * }]); + * ``` + */ + + +/** + * @ngdoc method + * @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 + * provider's `$get` property is a factory function that takes no arguments and returns the **value + * 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}. + * + * @param {string} name The name of the instance. + * @param {*} value The value. + * @returns {Object} registered provider instance + * + * @example + * Here are some examples of creating value services. + * ```js + * $provide.value('ADMIN_USER', 'admin'); + * + * $provide.value('RoleLookup', { admin: 0, writer: 1, reader: 2 }); + * + * $provide.value('halfOf', function(value) { + * return value / 2; + * }); + * ``` + */ + + +/** + * @ngdoc method + * @name $provide#constant + * @description + * + * 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}. + * + * @param {string} name The name of the constant. + * @param {*} value The constant value. + * @returns {Object} registered instance + * + * @example + * Here a some examples of creating constants: + * ```js + * $provide.constant('SHARD_HEIGHT', 306); + * + * $provide.constant('MY_COLOURS', ['red', 'blue', 'grey']); + * + * $provide.constant('double', function(value) { + * return value * 2; + * }); + * ``` + */ + + +/** + * @ngdoc method + * @name $provide#decorator + * @description + * + * 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|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 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()}. + * ```js + * $provide.decorator('$log', ['$delegate', function($delegate) { + * $delegate.warn = $delegate.error; + * return $delegate; + * }]); + * ``` + */ + + +function createInjector(modulesToLoad, strictDi) { + strictDi = (strictDi === true); + var INSTANTIATING = {}, + providerSuffix = 'Provider', + path = [], + loadedModules = new HashMap([], true), + 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); + } + 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; + + providerCache['$injector' + providerSuffix] = { $get: valueFn(protoInstanceInjector) }; + var runBlocks = loadModules(modulesToLoad); + instanceInjector = protoInstanceInjector.get('$injector'); + instanceInjector.strictDi = strictDi; + forEach(runBlocks, function(fn) { if (fn) instanceInjector.invoke(fn); }); + + return instanceInjector; + + //////////////////////////////////// + // $provider + //////////////////////////////////// + + function supportObject(delegate) { + return function(key, value) { + if (isObject(key)) { + forEach(key, reverseParams(delegate)); + } else { + return delegate(key, value); + } + }; + } + + function provider(name, provider_) { + assertNotHasOwnProperty(name, 'service'); + if (isFunction(provider_) || isArray(provider_)) { + provider_ = providerInjector.instantiate(provider_); + } + if (!provider_.$get) { + throw $injectorMinErr('pget', 'Provider \'{0}\' must define $get factory method.', name); + } + return (providerCache[name + providerSuffix] = provider_); + } + + 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) { + return $injector.instantiate(constructor); + }]); + } + + function value(name, val) { return factory(name, valueFn(val), false); } + + function constant(name, value) { + assertNotHasOwnProperty(name, 'constant'); + providerCache[name] = value; + instanceCache[name] = value; + } + + function decorator(serviceName, decorFn) { + var origProvider = providerInjector.get(serviceName + providerSuffix), + orig$get = origProvider.$get; + + origProvider.$get = function() { + var origInstance = instanceInjector.invoke(orig$get, origProvider); + return instanceInjector.invoke(decorFn, null, {$delegate: origInstance}); + }; + } + + //////////////////////////////////// + // Module Loading + //////////////////////////////////// + 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); + + 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); + runBlocks = runBlocks.concat(loadModules(moduleFn.requires)).concat(moduleFn._runBlocks); + runInvokeQueue(moduleFn._invokeQueue); + runInvokeQueue(moduleFn._configBlocks); + } else if (isFunction(module)) { + runBlocks.push(providerInjector.invoke(module)); + } else if (isArray(module)) { + runBlocks.push(providerInjector.invoke(module)); + } else { + assertArgFn(module, 'module'); + } + } catch (e) { + if (isArray(module)) { + module = module[module.length - 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. + // 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); + } + }); + return runBlocks; + } + + //////////////////////////////////// + // internal Injector + //////////////////////////////////// + + function createInternalInjector(cache, factory) { + + function getService(serviceName, caller) { + if (cache.hasOwnProperty(serviceName)) { + if (cache[serviceName] === INSTANTIATING) { + throw $injectorMinErr('cdep', 'Circular dependency found: {0}', + serviceName + ' <- ' + path.join(' <- ')); + } + return cache[serviceName]; + } else { + try { + path.unshift(serviceName); + cache[serviceName] = INSTANTIATING; + cache[serviceName] = factory(serviceName, caller); + return cache[serviceName]; + } catch (err) { + if (cache[serviceName] === INSTANTIATING) { + delete cache[serviceName]; + } + throw err; + } finally { + path.shift(); + } + } + } + + + 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); + } + args.push(locals && locals.hasOwnProperty(key) ? locals[key] : + getService(key, serviceName)); + } + return args; + } + + function isClass(func) { + // IE 9-11 do not support classes and IE9 leaks with the code below. + if (msie <= 11) { + return false; + } + // Support: Edge 12-13 only + // See: https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/6156135/ + return typeof func === 'function' + && /^(?:class\b|constructor\()/.test(stringifyFn(func)); + } + + 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) {}]); + 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: createInjector.$$annotate, + has: function(name) { + return providerCache.hasOwnProperty(name + providerSuffix) || cache.hasOwnProperty(name); + } + }; + } +} + +createInjector.$$annotate = annotate; + +/** + * @ngdoc provider + * @name $anchorScrollProvider + * @this + * + * @description + * 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 + // (using `Array#some()` instead of `angular#forEach()` since it's more performant + // and working in all supported browsers.) + function getFirstAnchor(list) { + var result = null; + Array.prototype.some.call(list, function(element) { + if (nodeName_(element) === 'a') { + result = element; + return true; + } + }); + return result; + } + + 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) scrollTo(null); + + // element with given id + else if ((elm = document.getElementById(hash))) scrollTo(elm); + + // first anchor with given name :-D + 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') 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(newVal, oldVal) { + // skip the initial scroll if $location.hash is empty + if (newVal === oldVal && newVal === '') return; + + jqLiteDocumentLoaded(function() { + $rootScope.$evalAsync(scroll); + }); + }); + } + + return scroll; + }]; +} + +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 HashMap(); + 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.remove(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.put(element, data); + postDigestElements.push(element); + + if (postDigestElements.length === 1) { + $rootScope.$$postDigest(handleCSSClassChanges); + } + } + } + }]; +}; + +/** + * @ngdoc provider + * @name $animateProvider + * + * @description + * Default implementation of $animate that doesn't perform any animations, instead just + * synchronously performs DOM updates and resolves the returned runner promise. + * + * In order to enable animations the `ngAnimate` module has to be loaded. + * + * To see the functional implementation check out `src/ngAnimate/animate.js`. + */ +var $AnimateProvider = ['$provide', /** @this */ function($provide) { + var provider = this; + + this.$$registeredAnimations = Object.create(null); + + /** + * @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, 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 { + * //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 (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'; + provider.$$registeredAnimations[name.substr(1)] = key; + $provide.factory(key, factory); + }; + + /** + * @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 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. + * @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 (this.$$classNameFilter) { + var reservedRegex = new RegExp('(\\s+|\\/)' + NG_ANIMATE_CLASSNAME + '(\\s+|\\/)'); + if (reservedRegex.test(this.$$classNameFilter.toString())) { + throw $animateMinErr('nongcls','$animateProvider.classNameFilter(regex) prohibits accepting a regex value which matches/contains the "{0}" CSS class.', NG_ANIMATE_CLASSNAME); + + } + } + } + return this.$$classNameFilter; + }; + + 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. + * + * 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. + * + * 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}. + */ + return { + // we don't call it directly since non-existant arguments may + // be interpreted as null within the sub enabled function + + /** + * + * @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). + */ + 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(); + } + }, + + /** + * + * @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 + */ + 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 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 {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, 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. + * + * @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 + */ + leave: function(element, options) { + return $$animateQueue.push(element, 'leave', prepareAnimateOptions(options), function() { + element.remove(); + }); + }, + + /** + * @ngdoc method + * @name $animate#addClass + * @kind function + * + * @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 + */ + addClass: function(element, className, options) { + options = prepareAnimateOptions(options); + options.addClass = mergeClasses(options.addclass, className); + return $$animateQueue.push(element, 'addClass', options); + }, + + /** + * @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', '$document', '$timeout', + function($q, $sniffer, $$animateAsyncRun, $document, $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) { + var doc = $document[0]; + + // the document may not be ready or attached + // to the module for some internal tests + if (doc && doc.hidden) { + 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 $browser + * @requires $log + * @description + * This object has two goals: + * + * - hide all the global state in the browser caused by the window object + * - abstract away all the browser specific features and inconsistencies + * + * For tests we provide {@link ngMock.$browser mock implementation} of the `$browser` + * service, which can be used for convenient testing of the application without the interaction with + * the real browser apis. + */ +/** + * @param {object} window The global window object. + * @param {object} document jQuery wrapped document. + * @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, + location = window.location, + history = window.history, + setTimeout = window.setTimeout, + clearTimeout = window.clearTimeout, + pendingDeferIds = {}; + + self.isMock = false; + + var outstandingRequestCount = 0; + var outstandingRequestCallbacks = []; + + // TODO(vojta): remove this temporary api + self.$$completeOutstandingRequest = completeOutstandingRequest; + self.$$incOutstandingRequestCount = function() { outstandingRequestCount++; }; + + /** + * Executes the `fn` function(supports currying) and decrements the `outstandingRequestCallbacks` + * counter. If the counter reaches 0, all the `outstandingRequestCallbacks` are executed. + */ + function completeOutstandingRequest(fn) { + try { + fn.apply(null, sliceArgs(arguments, 1)); + } finally { + outstandingRequestCount--; + if (outstandingRequestCount === 0) { + while (outstandingRequestCallbacks.length) { + try { + outstandingRequestCallbacks.pop()(); + } catch (e) { + $log.error(e); + } + } + } + } + } + + function getHash(url) { + var index = url.indexOf('#'); + return index === -1 ? '' : url.substr(index); + } + + /** + * @private + * Note: this method is used only by scenario runner + * TODO(vojta): prefix this method with $$ ? + * @param {function()} callback Function that will be called when no outstanding request + */ + self.notifyWhenNoOutstandingRequests = function(callback) { + if (outstandingRequestCount === 0) { + callback(); + } else { + outstandingRequestCallbacks.push(callback); + } + }; + + ////////////////////////////////////////////////////////////// + // URL API + ////////////////////////////////////////////////////////////// + + 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(); + lastHistoryState = cachedState; + + /** + * @name $browser#url + * + * @description + * GETTER: + * Without any argument, this method just returns current value of location.href. + * + * SETTER: + * With at least one argument, this method sets url to new value. + * If html5 history api supported, pushState/replaceState is used, otherwise + * location.href/location.replace is used. + * Returns its own instance to allow chaining + * + * NOTE: this api is intended for use only by the $location service. Please use the + * {@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 {object=} state object to use with pushState/replaceState + */ + 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) { + 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; + 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(); + // Do the assignment again so that those two variables are referentially identical. + lastHistoryState = cachedState; + } else { + if (!sameBase) { + pendingLocation = url; + } + if (replace) { + location.replace(url); + } 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 { + // - 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 pendingLocation || location.href.replace(/%27/g,'\''); + } + }; + + /** + * @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; + }; + + var urlChangeListeners = [], + urlChangeInit = false; + + function cacheStateAndFireUrlChange() { + pendingLocation = null; + cacheState(); + fireUrlChange(); + } + + // 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; + } + + function fireUrlChange() { + if (lastBrowserUrl === self.url() && lastHistoryState === cachedState) { + return; + } + + lastBrowserUrl = self.url(); + lastHistoryState = cachedState; + forEach(urlChangeListeners, function(listener) { + listener(self.url(), cachedState); + }); + } + + /** + * @name $browser#onUrlChange + * + * @description + * Register callback function that will be called, when url changes. + * + * It's only called when the url is changed from outside of angular: + * - user types different url into address bar + * - user clicks on history (forward/back) button + * - user clicks on a link + * + * It's not called when url is changed by $browser.url() method + * + * The listener gets called with new url as parameter. + * + * NOTE: this api is intended for use only by the $location service. Please use the + * {@link ng.$location $location service} to monitor url changes in angular apps. + * + * @param {function(string)} listener Listener function to be called when url changes. + * @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 + // changed by push/replaceState + + // html5 history api - popstate event + if ($sniffer.history) jqLite(window).on('popstate', cacheStateAndFireUrlChange); + // hashchange event + jqLite(window).on('hashchange', cacheStateAndFireUrlChange); + + urlChangeInit = true; + } + + urlChangeListeners.push(callback); + 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 = fireUrlChange; + + ////////////////////////////////////////////////////////////// + // Misc API + ////////////////////////////////////////////////////////////// + + /** + * @name $browser#baseHref + * + * @description + * Returns current + * (always relative - without domain) + * + * @returns {string} The current base href + */ + self.baseHref = function() { + var href = baseElement.attr('href'); + return href ? href.replace(/^(https?:)?\/\/[^/]*/, '') : ''; + }; + + /** + * @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()`. + * + * @description + * Executes a fn asynchronously via `setTimeout(fn, delay)`. + * + * Unlike when calling `setTimeout` directly, in test this function is mocked and instead of using + * `setTimeout` in tests, the fns are queued in an array, which can be programmatically flushed + * via `$browser.defer.flush()`. + * + */ + self.defer = function(fn, delay) { + var timeoutId; + outstandingRequestCount++; + timeoutId = setTimeout(function() { + delete pendingDeferIds[timeoutId]; + completeOutstandingRequest(fn); + }, delay || 0); + pendingDeferIds[timeoutId] = true; + return timeoutId; + }; + + + /** + * @name $browser#defer.cancel + * + * @description + * Cancels a deferred task identified with `deferId`. + * + * @param {*} deferId Token returned by the `$browser.defer` function. + * @returns {boolean} Returns `true` if the task hasn't executed yet and was successfully + * canceled. + */ + self.defer.cancel = function(deferId) { + if (pendingDeferIds[deferId]) { + delete pendingDeferIds[deferId]; + clearTimeout(deferId); + completeOutstandingRequest(noop); + return true; + } + return false; + }; + +} + +/** @this */ +function $BrowserProvider() { + this.$get = ['$window', '$log', '$sniffer', '$document', + function($window, $log, $sniffer, $document) { + return new Browser($window, $document, $log, $sniffer); + }]; +} + +/** + * @ngdoc service + * @name $cacheFactory + * @this + * + * @description + * Factory that constructs {@link $cacheFactory.Cache Cache} objects and gives access to + * them. + * + * ```js + * + * var cache = $cacheFactory('cacheId'); + * expect($cacheFactory.get('cacheId')).toBe(cache); + * expect($cacheFactory.get('noSuchCacheId')).not.toBeDefined(); + * + * cache.put("key", "value"); + * cache.put("another key", "another value"); + * + * // 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. + * @param {object=} options Options object that specifies the cache behavior. Properties: + * + * - `{number=}` `capacity` — turns the cache into LRU cache. + * + * @returns {object} Newly created cache object with the following set of methods: + * + * - `{object}` `info()` — Returns id, size, and options of cache. + * - `{{*}}` `put({string} key, {*} value)` — Puts a new key-value pair into the cache and returns + * it. + * - `{{*}}` `get({string} key)` — Returns cached value for `key` or undefined for cache miss. + * - `{void}` `remove({string} key)` — Removes a key-value pair from the cache. + * - `{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() { + + this.$get = function() { + var caches = {}; + + function cacheFactory(cacheId, options) { + if (cacheId in caches) { + throw minErr('$cacheFactory')('iid', 'CacheId \'{0}\' is already taken!', cacheId); + } + + var size = 0, + stats = extend({}, options, {id: cacheId}), + data = createMap(), + capacity = (options && options.capacity) || Number.MAX_VALUE, + lruHash = createMap(), + freshEnd = null, + staleEnd = null; + + /** + * @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) { + 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; + + if (size > capacity) { + this.remove(staleEnd.key); + } + + 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) { + if (capacity < Number.MAX_VALUE) { + var lruEntry = lruHash[key]; + + if (!lruEntry) return; + + 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) { + if (capacity < Number.MAX_VALUE) { + var lruEntry = lruHash[key]; + + if (!lruEntry) return; + + 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 data[key]; + size--; + }, + + + /** + * @ngdoc method + * @name $cacheFactory.Cache#removeAll + * @kind function + * + * @description + * Clears the cache object of any entries. + */ + removeAll: function() { + data = createMap(); + size = 0; + 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; + lruHash = null; + delete caches[cacheId]; + }, + + + /** + * @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 (!staleEnd) { + staleEnd = entry; + } else if (staleEnd === entry) { + staleEnd = entry.n; + } + + link(entry.n, entry.p); + link(entry, freshEnd); + freshEnd = entry; + freshEnd.n = null; + } + } + + + /** + * bidirectionally links two entries of the LRU linked list + */ + function link(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 + } + } + } + + + /** + * @ngdoc method + * @name $cacheFactory#info + * + * @description + * Get information about all the caches that have been created + * + * @returns {Object} - key-value map of `cacheId` to the result of calling `cache#info` + */ + cacheFactory.info = function() { + var info = {}; + forEach(caches, function(cache, cacheId) { + info[cacheId] = cache.info(); + }); + return info; + }; + + + /** + * @ngdoc method + * @name $cacheFactory#get + * + * @description + * Get access to a cache object by the `cacheId` used when it was created. + * + * @param {string} cacheId Name or id of a cache to access. + * @returns {object} Cache object identified by the cacheId or undefined if no such cache. + */ + cacheFactory.get = function(cacheId) { + return caches[cacheId]; + }; + + + return cacheFactory; + }; +} + +/** + * @ngdoc service + * @name $templateCache + * @this + * + * @description + * The first time a template is used, it is loaded in the template cache for quick retrieval. You + * can load templates directly into the cache in a `script` tag, or by consuming the + * `$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 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: + * + * ```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 component: + * ```js + * myApp.component('myComponent', { + * templateUrl: 'templateId.html' + * }); + * ``` + * + * or get it via the `$templateCache` service: + * ```js + * $templateCache.get('templateId.html') + * ``` + * + * See {@link ng.$cacheFactory $cacheFactory}. + * + */ +function $TemplateCacheProvider() { + this.$get = ['$cacheFactory', function($cacheFactory) { + return $cacheFactory('templates'); + }]; +} + +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * 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) + */ + + +/** + * @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#directive directives}. + * + *
      + * **Note:** This document is an in-depth reference of all directive options. + * For a gentle introduction to directives with examples of common use cases, + * see the {@link guide/directive directive guide}. + *
      + * + * ## Comprehensive Directive API + * + * There are many different options for a directive. + * + * The difference resides in the return value of the factory function. + * 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. + *
      + * + * Here's an example directive declared with a Directive Definition Object: + * + * ```js + * var myModule = angular.module(...); + * + * myModule.directive('directiveName', function factory(injectables) { + * var directiveDefinitionObject = { + * {@link $compile#-priority- priority}: 0, + * {@link $compile#-template- template}: '
      ', // or // function(tElement, tAttrs) { ... }, + * // or + * // {@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 { + * {@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 $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 $compile#-link- link}: function postLink( ... ) { ... } + * }; + * return directiveDefinitionObject; + * }); + * ``` + * + *
      + * **Note:** Any unspecified options will use the default value. You can see the default values below. + *
      + * + * Therefore the above can be simplified as: + * + * ```js + * var myModule = angular.module(...); + * + * myModule.directive('directiveName', function factory(injectables) { + * var directiveDefinitionObject = { + * link: function postLink(scope, iElement, iAttrs) { ... } + * }; + * return directiveDefinitionObject; + * // 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. + * * `$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 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 + * is necessary to specify the order in which the directives are applied. The `priority` is used + * to sort the directives before their `compile` functions get called. Priority is defined as a + * number. Directives with greater numerical `priority` are compiled first. Pre-link functions + * are also run in priority order, but post-link functions are run in reverse order. The order + * of directives with the same priority is undefined. The default priority is `0`. + * + * #### `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). Note that expressions + * and other directives used in the directive's template will also be excluded from execution. + * + * #### `scope` + * The scope property can be `false`, `true`, or an object: + * + * * **`false` (default):** No scope will be created for the directive. The directive will use its + * parent's scope. + * + * * **`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 element. 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. + * + * 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 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 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 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:** although bindings for non-ES6 class controllers are currently + * bound to `this` before the controller constructor is called, 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 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: + * `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` 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 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` + * 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 defaults (elements and attributes) are used. + * + * * `E` - Element name (default): `` + * * `A` - Attribute (default): `
      ` + * * `C` - Class: `
      ` + * * `M` - Comment: `` + * + * + * #### `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 ``. + * + * * `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` + * 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 + * $sce#getTrustedResourceUrl $sce.getTrustedResourceUrl}. + * + * + * #### `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 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` + * 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. 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. + * + * * `tAttrs` - template attributes - Normalized list of attributes declared on this element shared + * between all directive compile functions. + * + * * `transclude` - [*DEPRECATED*!] A transclude linking function: `function(scope, cloneLinkingFn)` + * + *
      + * **Note:** The template instance and the link instance may be different objects if the template has + * been cloned. For this reason it is **not** safe to do anything other than DOM transformations that + * 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. + *
      + + * A compile function can have a return value which can be either a function or an object. + * + * * returning a (post-link) function - is equivalent to registering the linking function via the + * `link` property of the config object when the compile function is empty. + * + * * returning an object with function(s) registered via `pre` and `post` properties - allows you to + * control when a linking function should be called during the linking phase. See info about + * pre-linking and post-linking functions below. + * + * + * #### `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 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 + * already been linked. + * + * * `iAttrs` - instance attributes - Normalized list of attributes declared on this element shared + * between all directive linking functions. + * + * * `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. + * 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 + * + * Executed before the child elements are linked. Not safe to do DOM transformation since the + * compiler linking function will fail to locate the correct elements for linking. + * + * #### 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 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. + * + * * *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 + * communication. + * + * * *Supports interpolation:* Interpolation attributes are assigned to the attribute object + * allowing other directives to read the interpolated value. + * + * * *Observing interpolated attributes:* Use `$observe` to observe the value changes of attributes + * that contain interpolation (e.g. `src="{{bar}}"`). Not only is this very efficient but it's also + * 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); + * + * // change the attribute + * attrs.$set('ngModel', 'new value'); + * + * // observe changes to interpolated attribute + * attrs.$observe('ngModel', function(value) { + * console.log('ngModel has changed value to ' + value); + * }); + * } + * ``` + * + * ## Example + * + *
      + * **Note**: Typically directives are registered with `module.directive`. The example below is + * to illustrate how `$compile` works. + *
      + * + + + +
      +
      +
      +
      +
      +
      + + it('should auto compile', function() { + 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 - 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=, 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: + * + * * `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. + * + * After linking the view is not updated until after a call to $digest which typically is done by + * Angular automatically. + * + * If you need access to the bound view, there are two ways to do it: + * + * - 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 = ....; + * + * var clonedElement = $compile(templateElement)(scope, function(clonedElement, scope) { + * //attach the clone to DOM document at the right place + * }); + * + * //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 provider + * @name $compileProvider + * + * @description + */ +$CompileProvider.$inject = ['$provide', '$$sanitizeUriProvider']; +/** @this */ +function $CompileProvider($provide, $$sanitizeUriProvider) { + var hasDirectives = {}, + 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 method + * @name $compileProvider#directive + * @kind function + * + * @description + * Register a new directive with the compiler. + * + * @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 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] = []; + $provide.factory(name + Suffix, ['$injector', '$exceptionHandler', + function($injector, $exceptionHandler) { + var directives = []; + forEach(hasDirectives[name], function(directiveFactory, index) { + try { + var directive = $injector.invoke(directiveFactory); + if (isFunction(directive)) { + directive = { compile: valueFn(directive) }; + } else if (!directive.compile && directive.link) { + directive.compile = valueFn(directive.link); + } + directive.priority = directive.priority || 0; + directive.index = index; + directive.name = directive.name || name; + directive.require = getDirectiveRequire(directive); + directive.restrict = getDirectiveRestrict(directive.restrict, name); + directive.$$moduleName = directiveFactory.$$moduleName; + directives.push(directive); + } catch (e) { + $exceptionHandler(e); + } + }); + return directives; + }]); + } + hasDirectives[name].push(directiveFactory); + } else { + forEach(name, reverseParams(registerDirective)); + } + return this; + }; + + /** + * @ngdoc method + * @name $compileProvider#component + * @module ng + * @param {string} name Name of the component in camelCase (i.e. `myComp` which will match ``) + * @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) { + 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 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 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` + * regular expression. If a match is found, the original url is written into the dom. Otherwise, + * the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM. + * + * @param {RegExp=} regexp New regexp to whitelist urls with. + * @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for + * chaining otherwise. + */ + this.aHrefSanitizationWhitelist = function(regexp) { + if (isDefined(regexp)) { + $$sanitizeUriProvider.aHrefSanitizationWhitelist(regexp); + return this; + } else { + return $$sanitizeUriProvider.aHrefSanitizationWhitelist(); + } + }; + + + /** + * @ngdoc method + * @name $compileProvider#imgSrcSanitizationWhitelist + * @kind function + * + * @description + * Retrieves or overrides the default regular expression that is used for whitelisting of safe + * urls during img[src] sanitization. + * + * The sanitization is a security measure aimed at prevent XSS attacks via html links. + * + * Any url about to be assigned to img[src] via data-binding is first normalized and turned into + * an absolute url. Afterwards, the url is matched against the `imgSrcSanitizationWhitelist` + * regular expression. If a match is found, the original url is written into the dom. Otherwise, + * the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM. + * + * @param {RegExp=} regexp New regexp to whitelist urls with. + * @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for + * chaining otherwise. + */ + this.imgSrcSanitizationWhitelist = function(regexp) { + if (isDefined(regexp)) { + $$sanitizeUriProvider.imgSrcSanitizationWhitelist(regexp); + return this; + } else { + return $$sanitizeUriProvider.imgSrcSanitizationWhitelist(); + } + }; + + /** + * @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 true in Angular 1.5.x but will switch to false in Angular 1.6.x. + */ + var preAssignBindingsEnabled = true; + this.preAssignBindingsEnabled = function(enabled) { + if (isDefined(enabled)) { + preAssignBindingsEnabled = enabled; + return this; + } + return preAssignBindingsEnabled; + }; + + + 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 = {}; + } + + this.$$element = element; + } + + 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 method + * @name $compile.directive.Attributes#$addClass + * @kind function + * + * @description + * Adds the CSS class value specified by the classVal parameter to the element. If animations + * are enabled then an animation will be triggered for the class addition. + * + * @param {string} classVal The className value that will be added to the element + */ + $addClass: function(classVal) { + if (classVal && classVal.length > 0) { + $animate.addClass(this.$$element, classVal); + } + }, + + /** + * @ngdoc method + * @name $compile.directive.Attributes#$removeClass + * @kind function + * + * @description + * Removes the CSS class value specified by the classVal parameter from the element. If + * animations are enabled then an animation will be triggered for the class removal. + * + * @param {string} classVal The className value that will be removed from the element + */ + $removeClass: function(classVal) { + if (classVal && classVal.length > 0) { + $animate.removeClass(this.$$element, classVal); + } + }, + + /** + * @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 + * between the new and old CSS class values (specified as newClasses and oldClasses). + * + * @param {string} newClasses The current CSS className value + * @param {string} oldClasses The former CSS className value + */ + $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); + } + }, + + /** + * Set a normalized attribute on the element in a way such that all directives + * can share the attribute. This function properly handles boolean attributes. + * @param {string} key Normalized key. (ie ngAttribute) + * @param {string|boolean} value The value to set. If `null` attribute will be deleted. + * @param {boolean=} writeAttr If false, does not write the value to DOM element attribute. + * Defaults to true. + * @param {string=} attrName Optional none normalized name. Defaults to key. + */ + $set: function(key, value, writeAttr, attrName) { + // TODO: decide whether or not to throw an error if "class" + //is set through this function since it may cause $updateClass to + //become unstable. + + 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; + + // translate normalized key to actual key + if (attrName) { + this.$attr[key] = attrName; + } else { + attrName = this.$attr[key]; + if (!attrName) { + this.$attr[key] = attrName = snake_case(key, '-'); + } + } + + nodeName = nodeName_(this.$$element); + + 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 || isUndefined(value)) { + this.$$element.removeAttr(attrName); + } else { + if (SIMPLE_ATTR_NAME.test(attrName)) { + this.$$element.attr(attrName, value); + } else { + setSpecialAttr(this.$$element[0], attrName, value); + } + } + } + + // fire observers + var $$observers = this.$$observers; + if ($$observers) { + forEach($$observers[observer], function(fn) { + try { + fn(value); + } catch (e) { + $exceptionHandler(e); + } + }); + } + }, + + + /** + * @ngdoc method + * @name $compile.directive.Attributes#$observe + * @kind function + * + * @description + * Observes an interpolated attribute. + * + * The observer function will be invoked once during the next `$digest` following + * compilation. The observer is then invoked whenever the interpolated value + * changes. + * + * @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/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 = createMap())), + listeners = ($$observers[key] || ($$observers[key] = [])); + + listeners.push(fn); + $rootScope.$evalAsync(function() { + if (!listeners.$$inter && attrs.hasOwnProperty(key) && !isUndefined(attrs[key])) { + // no one registered attribute interpolation function, so lets call it manually + fn(attrs[key]); + } + }); + + 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) { + return template.replace(/\{\{/g, startSymbol).replace(/}}/g, endSymbol); + }, + 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; + + //================================ + + function compile($compileNodes, transcludeFn, maxPriority, ignoreDirective, + previousCompileContext) { + if (!($compileNodes instanceof jqLite)) { + // jquery always rewraps, whereas we need to preserve the original selector so that we can + // modify it. + $compileNodes = jqLite($compileNodes); + } + + var NOT_EMPTY = /\S+/; + + // 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 + for (var i = 0, len = $compileNodes.length; i < len; i++) { + var domNode = $compileNodes[i]; + + if (domNode.nodeType === NODE_TYPE_TEXT && domNode.nodeValue.match(NOT_EMPTY) /* non-empty */) { + jqLiteWrapNode(domNode, $compileNodes[i] = window.document.createElement('span')); + } + } + + var compositeLinkFn = + compileNodes($compileNodes, transcludeFn, $compileNodes, + maxPriority, ignoreDirective, previousCompileContext); + compile.$$addScopeClass($compileNodes); + var namespace = null; + return function publicLinkFn(scope, cloneConnectFn, options) { + assertArg(scope, 'scope'); + + 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(); + } + + options = options || {}; + var parentBoundTranscludeFn = options.parentBoundTranscludeFn, + transcludeControllers = options.transcludeControllers, + futureParentElement = options.futureParentElement; + + // When `parentBoundTranscludeFn` is passed, it is a + // `controllersBoundTransclude` function (it was previously passed + // as `transclude` to directive.link) so we must unwrap it to get + // its `boundTranscludeFn` + if (parentBoundTranscludeFn && parentBoundTranscludeFn.$$boundTransclude) { + parentBoundTranscludeFn = parentBoundTranscludeFn.$$boundTransclude; + } + + if (!namespace) { + namespace = detectNamespaceForChildElements(futureParentElement); + } + var $linkNode; + if (namespace !== 'html') { + // When using a directive with replace:true and templateUrl the $compileNodes + // (or a child element inside of them) + // might change, so we need to recreate the namespace adapted compileNodes + // for call to the link function. + // Note: This will already clone the nodes... + $linkNode = jqLite( + wrapTemplate(namespace, jqLite('
      ').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, parentBoundTranscludeFn); + return $linkNode; + }; + } + + 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'; + } + } + + /** + * Compile function matches each node in nodeList against the directives. Once all directives + * for a particular node are collected their compile functions are executed. The compile + * functions return values - the linking functions - are combined into a composite linking + * 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 + * 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. + */ + function compileNodes(nodeList, transcludeFn, $rootElement, maxPriority, ignoreDirective, + previousCompileContext) { + var linkFns = [], + 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. + directives = collectDirectives(nodeList[i], [], attrs, i === 0 ? maxPriority : undefined, + ignoreDirective); + + nodeLinkFn = (directives.length) + ? applyDirectivesToNode(directives, nodeList[i], attrs, transcludeFn, $rootElement, + null, [], [], previousCompileContext) + : null; + + if (nodeLinkFn && nodeLinkFn.scope) { + compile.$$addScopeClass(attrs.$$element); + } + + childLinkFn = (nodeLinkFn && nodeLinkFn.terminal || + !(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; + } + + //use the previous context only for the first element in the virtual group + previousCompileContext = null; + } + + // return a linking function if we have found anything, null otherwise + return linkFnFound ? compositeLinkFn : null; + + function compositeLinkFn(scope, nodeList, $rootElement, parentBoundTranscludeFn) { + var nodeLinkFn, childLinkFn, node, childScope, i, ii, idx, childBoundTranscludeFn; + var stableNodeList; + + + 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, ii = linkFns.length; i < ii;) { + node = stableNodeList[linkFns[i++]]; + nodeLinkFn = linkFns[i++]; + childLinkFn = linkFns[i++]; + + if (nodeLinkFn) { + if (nodeLinkFn.scope) { + childScope = scope.$new(); + compile.$$addScopeInfo(jqLite(node), childScope); + } else { + childScope = scope; + } + + 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 { + childBoundTranscludeFn = null; + } + + nodeLinkFn(childLinkFn, childScope, node, $rootElement, childBoundTranscludeFn); + + } else if (childLinkFn) { + childLinkFn(scope, node.childNodes, undefined, parentBoundTranscludeFn); + } + } + } + } + + function createBoundTranscludeFn(scope, transcludeFn, previousBoundTranscludeFn) { + function boundTranscludeFn(transcludedScope, cloneFn, controllers, futureParentElement, containingScope) { + + if (!transcludedScope) { + transcludedScope = scope.$new(false, containingScope); + transcludedScope.$$transcluded = true; + } + + 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 boundTranscludeFn; + } + + /** + * Looks for directives on the given node and adds them to the directive collection which is + * sorted. + * + * @param node Node to search. + * @param directives An array to which the directives are added to. This array is sorted before + * the function returns. + * @param attrs The shared attrs object which is used to populate the normalized attributes. + * @param {number=} maxPriority Max directive priority. + */ + function collectDirectives(node, directives, attrs, maxPriority, ignoreDirective) { + var nodeType = node.nodeType, + attrsMap = attrs.$attr, + match, + nodeName, + className; + + switch (nodeType) { + case NODE_TYPE_ELEMENT: /* Element */ + + nodeName = nodeName_(node); + + // use the node name: + addDirective(directives, + directiveNormalize(nodeName), 'E', maxPriority, ignoreDirective); + + // iterate over the attributes + 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]; + name = attr.name; + value = trim(attr.value); + + // 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(); + }); + } + + 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, 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))) { + nName = directiveNormalize(match[2]); + if (addDirective(directives, nName, 'C', maxPriority, ignoreDirective)) { + attrs[nName] = trim(match[3]); + } + className = className.substr(match.index + match[0].length); + } + } + break; + case NODE_TYPE_TEXT: /* Text Node */ + if (msie === 11) { + // Workaround for #11781 + while (node.parentNode && node.nextSibling && node.nextSibling.nodeType === NODE_TYPE_TEXT) { + node.nodeValue = node.nodeValue + node.nextSibling.nodeValue; + node.parentNode.removeChild(node.nextSibling); + } + } + addTextInterpolateDirective(directives, node.nodeValue); + break; + case NODE_TYPE_COMMENT: /* Comment */ + if (!commentDirectivesEnabled) break; + collectCommentDirectives(node, directives, attrs, maxPriority, ignoreDirective); + break; + } + + directives.sort(byPriority); + 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 a directive-start it collects all of the siblings until it finds + * directive-end. + * @param node + * @param attrStart + * @param attrEnd + * @returns {*} + */ + function groupScan(node, attrStart, attrEnd) { + var nodes = []; + var depth = 0; + if (attrStart && node.hasAttribute && node.hasAttribute(attrStart)) { + do { + if (!node) { + throw $compileMinErr('uterdir', + 'Unterminated attribute, found \'{0}\' but no matching \'{1}\' found.', + attrStart, attrEnd); + } + if (node.nodeType === NODE_TYPE_ELEMENT) { + if (node.hasAttribute(attrStart)) depth++; + if (node.hasAttribute(attrEnd)) depth--; + } + nodes.push(node); + node = node.nextSibling; + } while (depth > 0); + } else { + nodes.push(node); + } + + return jqLite(nodes); + } + + /** + * Wrapper for linking function which converts normal linking function into a grouped + * linking function. + * @param linkFn + * @param attrStart + * @param attrEnd + * @returns {Function} + */ + function groupElementsLinkFnWrapper(linkFn, attrStart, attrEnd) { + 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 + * of the directives if the terminal directive has been reached. + * + * @param {Array} directives Array of collected directives to execute their compile function. + * 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 + * 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 + * argument has the root jqLite array so that we can replace nodes + * on it. + * @param {Object=} originalReplaceDirective An optional directive that will be ignored when + * compiling the transclusion. + * @param {Array.} preLinkFns + * @param {Array.} postLinkFns + * @param {Object} previousCompileContext Context used for previous compilation of the current + * node + * @returns {Function} linkFn + */ + function applyDirectivesToNode(directives, compileNode, templateAttrs, transcludeFn, + jqCollection, originalReplaceDirective, preLinkFns, postLinkFns, + previousCompileContext) { + previousCompileContext = previousCompileContext || {}; + + var terminalPriority = -Number.MAX_VALUE, + 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++) { + directive = directives[i]; + var attrStart = directive.$$start; + var attrEnd = directive.$$end; + + // collect multiblock sections + if (attrStart) { + $compileNode = groupScan(compileNode, attrStart, attrEnd); + } + $template = undefined; + + if (terminalPriority > directive.priority) { + break; // prevent further processing of directives + } + + 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) { + 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) { + controllerDirectives = controllerDirectives || createMap(); + assertNoDuplicate('\'' + directiveName + '\' controller', + controllerDirectives[directiveName], directive, $compileNode); + controllerDirectives[directiveName] = directive; + } + + 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 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') { + hasElementTranscludeDirective = true; + terminalPriority = directive.priority; + $template = $compileNode; + $compileNode = templateAttrs.$$element = + jqLite(compile.$$createComment(directiveName, templateAttrs[directiveName])); + compileNode = $compileNode[0]; + replaceWith(jqCollection, sliceArgs($template), compileNode); + + // 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 { + + var slots = createMap(); + + $template = jqLite(jqLiteClone(compileNode)).contents(); + + if (isObject(directiveValue)) { + + // 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 = 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; + + directiveValue = denormalizeTemplate(directiveValue); + + if (directive.replace) { + replaceDirective = directive; + if (jqLiteIsTextNode(directiveValue)) { + $template = []; + } else { + $template = removeComments(wrapTemplate(directive.templateNamespace, trim(directiveValue))); + } + 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}', + directiveName, ''); + } + + replaceWith(jqCollection, $compileNode, compileNode); + + var newTemplateAttrs = {$attr: {}}; + + // combine directives from the original node and from the template: + // - take the array of directives for this element + // - split it into two parts, those that already applied (processed) and those that weren't (unprocessed) + // - collect directives from the template and sort them by priority + // - combine directives as: processed + template + unprocessed + var templateDirectives = collectDirectives(compileNode, [], newTemplateAttrs); + var unprocessedDirectives = directives.splice(i + 1, directives.length - (i + 1)); + + 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); + + ii = directives.length; + } else { + $compileNode.html(directiveValue); + } + } + + if (directive.templateUrl) { + hasTemplate = true; + assertNoDuplicate('template', templateDirective, directive, $compileNode); + templateDirective = directive; + + if (directive.replace) { + replaceDirective = directive; + } + + // eslint-disable-next-line no-func-assign + nodeLinkFn = compileTemplateUrl(directives.splice(i, directives.length - i), $compileNode, + 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, bind(context, linkFn), attrStart, attrEnd); + } else if (linkFn) { + addLinkFns(bind(context, linkFn.pre), bind(context, linkFn.post), attrStart, attrEnd); + } + } catch (e) { + $exceptionHandler(e, startingTag($compileNode)); + } + } + + if (directive.terminal) { + nodeLinkFn.terminal = true; + terminalPriority = Math.max(terminalPriority, directive.priority); + } + + } + + nodeLinkFn.scope = newScopeDirective && newScopeDirective.scope === true; + 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; + + //////////////////// + + function addLinkFns(pre, post, attrStart, attrEnd) { + 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}); + } + preLinkFns.push(pre); + } + 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}); + } + postLinkFns.push(post); + } + } + + function nodeLinkFn(childLinkFn, scope, linkNode, $rootElement, boundTranscludeFn) { + var i, ii, linkFn, isolateScope, controllerScope, elementControllers, transcludeFn, $element, + attrs, scopeBindingInfo; + + if (compileNode === linkNode) { + attrs = templateAttrs; + $element = templateAttrs.$$element; + } else { + $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); + } + + if (newIsolateScopeDirective) { + // 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); + } + } + + // 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 = {}; + } + + 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); + } + } 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++) { + linkFn = preLinkFns[i]; + invokeLinkFn(linkFn, + linkFn.isolateScope ? isolateScope : scope, + $element, + attrs, + linkFn.require && getControllers(linkFn.directiveName, linkFn.require, $element, elementControllers), + transcludeFn + ); + } + + // RECURSION + // We only pass the isolate scope, if the isolate directive has a template, + // otherwise the child elements do not belong to the isolate directive. + var scopeToChild = scope; + if (newIsolateScopeDirective && (newIsolateScopeDirective.template || newIsolateScopeDirective.templateUrl === null)) { + scopeToChild = isolateScope; + } + if (childLinkFn) { + childLinkFn(scopeToChild, linkNode.childNodes, undefined, boundTranscludeFn); + } + + // 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 + ); + } + + // 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; + } + + if (hasElementTranscludeDirective) { + transcludeControllers = elementControllers; + } + 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 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: isolateScope, $$newScope: newScope}); + } + } + + /** + * looks up the directive and decorates it with exception handling and proper parameters. We + * call this the boundDirective. + * + * @param {string} name name of the directive to look up. + * @param {string} location The directive must be found in specific format. + * String containing any of theses characters: + * + * * `E`: element name + * * `A': attribute + * * `C`: class + * * `M`: comment + * @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 < 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}); + } + 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. + * The desired effect is to have both of the attributes present. + * + * @param {object} dst destination attributes (original DOM) + * @param {object} src source attributes (from the directive template) + */ + function mergeTemplateAttributes(dst, src) { + var srcAttr = src.$attr, + dstAttr = dst.$attr; + + // reapply the old attributes to the new element + forEach(dst, function(value, key) { + if (key.charAt(0) !== '$') { + if (src[key] && src[key] !== value) { + value += (key === 'style' ? ';' : ' ') + src[key]; + } + dst.$set(key, value, true, srcAttr[key]); + } + }); + + // copy the new attributes on the old attrs object + forEach(src, function(value, 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; + + if (key !== 'class' && key !== 'style') { + dstAttr[key] = srcAttr[key]; + } + } + }); + } + + + function compileTemplateUrl(directives, $compileNode, tAttrs, + $rootElement, childTranscludeFn, preLinkFns, postLinkFns, previousCompileContext) { + var linkQueue = [], + 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(); + + $templateRequest(templateUrl) + .then(function(content) { + var compileNode, tempTemplateAttrs, $template, childBoundTranscludeFn; + + content = denormalizeTemplate(content); + + if (origAsyncDirective.replace) { + if (jqLiteIsTextNode(content)) { + $template = []; + } else { + $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); + } + + tempTemplateAttrs = {$attr: {}}; + replaceWith($rootElement, $compileNode, compileNode); + var templateDirectives = collectDirectives(compileNode, [], tempTemplateAttrs); + + 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); + + 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); + + 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); + + // Copy in CSS classes from original node + safeAddClass(jqLite(linkNode), oldClasses); + } + if (afterTemplateNodeLinkFn.transcludeOnThisElement) { + childBoundTranscludeFn = createBoundTranscludeFn(scope, afterTemplateNodeLinkFn.transclude, boundTranscludeFn); + } else { + childBoundTranscludeFn = boundTranscludeFn; + } + afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, linkNode, $rootElement, + childBoundTranscludeFn); + } + linkQueue = null; + }); + + return function delayedNodeLinkFn(ignoreChildLinkFn, scope, node, rootElement, boundTranscludeFn) { + var childBoundTranscludeFn = boundTranscludeFn; + if (scope.$$destroyed) return; + if (linkQueue) { + linkQueue.push(scope, + node, + rootElement, + childBoundTranscludeFn); + } else { + if (afterTemplateNodeLinkFn.transcludeOnThisElement) { + childBoundTranscludeFn = createBoundTranscludeFn(scope, afterTemplateNodeLinkFn.transclude, boundTranscludeFn); + } + afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, node, rootElement, childBoundTranscludeFn); + } + }; + } + + + /** + * Sorting function for bound directives. + */ + function byPriority(a, b) { + var diff = b.priority - a.priority; + if (diff !== 0) return diff; + if (a.name !== b.name) return (a.name < b.name) ? -1 : 1; + 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}, {2}{3}] asking for {4} on: {5}', + previousDirective.name, wrapModuleNameIfDefined(previousDirective.$$moduleName), + directive.name, wrapModuleNameIfDefined(directive.$$moduleName), what, startingTag(element)); + } + } + + + function addTextInterpolateDirective(directives, text) { + var interpolateFn = $interpolate(text, true); + if (interpolateFn) { + directives.push({ + priority: 0, + 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') { + return $sce.HTML; + } + var tag = nodeName_(node); + // 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') + ) { + return $sce.RESOURCE_URL; + } + } + + + 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)); + } + + directives.push({ + priority: 100, + compile: function() { + return { + pre: function attrInterpolatePreLinkFn(scope, element, attr) { + 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; + } + + // if attribute was updated so that there is no interpolation going on we don't want to + // register any observers + if (!interpolateFn) return; + + // 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); + } + }); + } + }; + } + }); + } + + + /** + * This is a special jqLite.replaceWith, which can replace items which + * have no parents, provided that the containing jqLite collection is provided. + * + * @param {JqLite=} $rootElement The root of the compile tree. Used so that we can replace nodes + * in the root of the tree. + * @param {JqLite} elementsToRemove The jqLite element which we are going to replace. We keep + * the shell, but replace its DOM node reference. + * @param {Node} newNode The new DOM node. + */ + function replaceWith($rootElement, elementsToRemove, newNode) { + var firstElementToRemove = elementsToRemove[0], + removeCount = elementsToRemove.length, + parent = firstElementToRemove.parentNode, + i, ii; + + if ($rootElement) { + 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; + j < jj; j++, j2++) { + if (j2 < jj) { + $rootElement[j] = $rootElement[j2]; + } else { + delete $rootElement[j]; + } + } + $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; + } + } + } + + if (parent) { + parent.replaceChild(newNode, firstElementToRemove); + } + + // 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; + } + + + 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)); + } + } + + + // 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)) { + 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; + attrs[attrName] = undefined; + } + if (optional && !attrs[attrName]) break; + + parentGet = $parse(attrs[attrName]); + if (parentGet.literal) { + compare = equals; + } else { + // eslint-disable-next-line no-self-compare + compare = function simpleCompare(a, b) { return a === b || (a !== a && b !== b); }; + } + 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; + 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 '&': + // 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) && currentValue !== previousValue && + // eslint-disable-next-line no-self-compare + (currentValue === currentValue || previousValue === 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](); + } + } + }; + } + }]; +} + +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; +/** + * Converts all accepted directives format into proper directive name. + * @param name Name to normalize + */ +function directiveNormalize(name) { + return camelCase(name.replace(PREFIX_REGEXP, '')); +} + +/** + * @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 $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 method + * @name $compile.directive.Attributes#$set + * @kind function + * + * @description + * Set DOM element attribute value. + * + * + * @param {string} name Normalized element attribute name of the property to modify. The name is + * reverse-translated using the {@link ng.$compile.directive.Attributes#$attr $attr} + * property to the original name. + * @param {string} value Value to set the attribute to. The value can be an interpolated string. + */ + + + +/** + * Closure compiler type information + */ + +function nodesetLinkingFn( + /* angular.Scope */ scope, + /* NodeList */ nodeList, + /* Element */ rootElement, + /* function(Function) */ boundTranscludeFn +) {} + +function directiveLinkingFn( + /* nodesetLinkingFn */ nodesetLinkingFn, + /* angular.Scope */ scope, + /* Node */ node, + /* Element */ rootElement, + /* function(Function) */ boundTranscludeFn +) {} + +function tokenDifference(str1, str2) { + var values = '', + 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; + } + 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 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#register register} method. + */ +function $ControllerProvider() { + var controllers = {}, + globals = false; + + /** + * @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 + * annotations in the array notation). + */ + this.register = function(name, constructor) { + assertNotHasOwnProperty(name, 'controller'); + if (isObject(name)) { + extend(controllers, name); + } else { + controllers[name] = constructor; + } + }; + + /** + * @ngdoc method + * @name $controllerProvider#allowGlobals + * + * @deprecated + * sinceVersion="v1.3.0" + * removeVersion="v1.7.0" + * This method of finding controllers has been deprecated. + * + * @description If called, allows `$controller` to find controller constructors on `window` * + */ + this.allowGlobals = function() { + globals = true; + }; + + + this.$get = ['$injector', '$window', function($injector, $window) { + + /** + * @ngdoc service + * @name $controller + * @requires $injector + * + * @param {Function|string} constructor If called with a function then it's considered to be the + * controller constructor function. Otherwise it's considered to be a string which is used + * to retrieve the controller constructor using the following steps: + * + * * check if a controller with given name is registered via `$controllerProvider` + * * check if evaluating the string on the current scope returns a constructor + * * if $controllerProvider#allowGlobals, check `window[constructor]` on the global + * `window` object (not recommended) + * + * The string can use the `controller as property` syntax, where the controller instance is published + * as the specified property on the `scope`; the `scope` must be injected into `locals` param for this + * to work correctly. + * + * @param {Object} locals Injection locals for Controller. + * @return {Object} Instance of given controller. + * + * @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 [BC version](https://gist.github.com/1649788). + */ + 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); + 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) || + (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); + } + + 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) { + addIdentifier(locals, identifier, instance, constructor || expression.name); + } + + 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); + }]; +} + +/** + * @ngdoc service + * @name $exceptionHandler + * @requires ng.$log + * @this + * + * @description + * Any uncaught exception in angular expressions is delegated to this service. + * The default implementation simply delegates to `$log.error` which logs it into + * the browser console. + * + * In unit tests, if `angular-mocks.js` is loaded, this service is overridden by + * {@link ngMock.$exceptionHandler mock $exceptionHandler} which aids in testing. + * + * ## Example: + * + * 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()`. + * + * ```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 + * the error was thrown. + * + */ +function $ExceptionHandlerProvider() { + this.$get = ['$log', function($log) { + return function(exception, cause) { + $log.error.apply($log, arguments); + }; + }]; +} + +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'); +var $httpMinErrLegacyFn = function(method) { + return function() { + throw $httpMinErr('legacy', 'The method `{0}` on the promise returned from `$http` has been disabled.', method); + }; +}; + +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)) 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'); + if ((contentType && (contentType.indexOf(APPLICATION_JSON) === 0)) || isJsonLike(tempData)) { + data = fromJson(tempData); + } + } + } + + 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 + * + * @param {string} headers Raw headers as a string + * @returns {Object} Parsed headers as key value object + */ +function parseHeaders(headers) { + var parsed = createMap(), i; + + function fillInParsed(key, val) { + if (key) { + 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; +} + + +/** + * Returns a function that provides access to parsed headers. + * + * Headers are lazy parsed when first requested. + * @see parseHeaders + * + * @param {(string|Object)} headers Headers to provide access to. + * @returns {function(string=)} Returns a getter function which if called with: + * + * - 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; + + return function(name) { + if (!headersObj) headersObj = parseHeaders(headers); + + if (name) { + var value = headersObj[lowercase(name)]; + if (value === undefined) { + value = null; + } + return value; + } + + return headersObj; + }; +} + + +/** + * Chain all given functions + * + * This function is used for both request and response transforming + * + * @param {*} data Data to transform. + * @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, status, fns) { + if (isFunction(fns)) { + return fns(data, headers, status); + } + + forEach(fns, function(fn) { + data = fn(data, headers, status); + }); + + return data; +} + + +function isSuccess(status) { + return 200 <= status && status < 300; +} + + +/** + * @ngdoc provider + * @name $httpProvider + * @this + * + * @description + * Use `$httpProvider` to change the default behavior of the {@link ng.$http $http} service. + * */ +function $HttpProvider() { + /** + * @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.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'`. + * + * - **`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.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}. + * + **/ + var defaults = this.defaults = { + // transform incoming response data + transformResponse: [defaultHttpResponseTransform], + + // transform outgoing request data + transformRequest: [function(d) { + return isObject(d) && !isFile(d) && !isBlob(d) && !isFormData(d) ? toJson(d) : d; + }], + + // default headers + headers: { + common: { + 'Accept': 'application/json, text/plain, */*' + }, + 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', + + paramSerializer: '$httpParamSerializer' + }; + + 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; + }; + + var useLegacyPromise = true; + /** + * @ngdoc method + * @name $httpProvider#useLegacyPromiseExtensions + * @description + * + * @deprecated + * sinceVersion="v1.4.4" + * removeVersion="v1.6.0" + * This method will be removed in v1.6.0 along with the legacy promise methods. + * + * Configure `$http` service to return promises without the shorthand methods `success` and `error`. + * This should be used to make sure that applications work without these methods. + * + * Defaults to true. If no value is specified, returns the current configured value. + * + * @param {boolean=} value If true, `$http` will return a promise with the deprecated legacy `success` and `error` methods. + * + * @returns {boolean|Object} If a value is specified, returns the $httpProvider for chaining. + * otherwise, returns the current configured value. + **/ + this.useLegacyPromiseExtensions = function(value) { + if (isDefined(value)) { + useLegacyPromise = !!value; + return this; + } + return useLegacyPromise; + }; + + /** + * @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 = []; + + this.$get = ['$httpBackend', '$$cookieReader', '$cacheFactory', '$rootScope', '$q', '$injector', + function($httpBackend, $$cookieReader, $cacheFactory, $rootScope, $q, $injector) { + + 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 + * server request. + */ + var reversedInterceptors = []; + + forEach(interceptorFactories, function(interceptorFactory) { + reversedInterceptors.unshift(isString(interceptorFactory) + ? $injector.get(interceptorFactory) : $injector.invoke(interceptorFactory)); + }); + + /** + * @ngdoc service + * @kind function + * @name $http + * @requires ng.$httpBackend + * @requires $cacheFactory + * @requires $rootScope + * @requires $q + * @requires $injector + * + * @description + * The `$http` service is a core Angular service that facilitates communication with the remote + * 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. + * + * 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 {@link $http#usage configuration object} — + * that is used to generate an HTTP request and returns a {@link ng.$q promise}. + * + * ```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 + * }, function errorCallback(response) { + * // called asynchronously if an error occurs + * // or server returns response with an error status. + * }); + * ``` + * + * 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. + * + * 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); + * ``` + * + * Complete list of shortcut methods: + * + * - {@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} + * + * + * ## 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(); + * ``` + * + * ## Deprecation Notice + *
      + * The `$http` legacy promise methods `success` and `error` have been deprecated and will be + * removed in v1.6.0. + * Use the standard `then` method instead. + * If {@link $httpProvider#useLegacyPromiseExtensions `$httpProvider.useLegacyPromiseExtensions`} is set to + * `false` then these methods will throw {@link $http:legacy `$http/legacy`} error. + *
      + * + * ## 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, \*/\* + * - `$httpProvider.defaults.headers.post`: (header defaults for POST requests) + * - `Content-Type: application/json` + * - `$httpProvider.defaults.headers.put` (header defaults for PUT requests) + * - `Content-Type: application/json` + * + * 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' }`. + * + * 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.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: + * + * ```js + * var req = { + * method: 'POST', + * url: 'http://example.com', + * headers: { + * 'Content-Type': undefined + * }, + * data: { test: 'test' } + * } + * + * $http(req).then(function(){...}, function(){...}); + * ``` + * + * ## 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`): + * + * - If the `data` property of the request configuration object contains an object, serialize it + * into JSON format. + * + * Response transformations (`$httpProvider.defaults.transformResponse` and `$http.defaults.transformResponse`): + * + * - If XSRF prefix is detected, strip it (see Security Considerations section below). + * - If JSON response is detected, deserialize it using a JSON parser. + * + * + * ### 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. + * + * The following code demonstrates adding a new response transformation to be run after the default response + * transformations have been run. + * + * ```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); + * } + * + * $http({ + * url: '...', + * method: 'GET', + * transformResponse: appendTransform($http.defaults.transformResponse, function(value) { + * return doTransform(value); + * }) + * }); + * ``` + * + * + * ## Caching + * + * {@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. + * + * 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}. + * + * For purposes of global error handling, authentication, or any kind of synchronous or + * asynchronous pre-processing of request or postprocessing of responses, it is desirable to be + * 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. + * + * The interceptors are service factories that are registered with the `$httpProvider` by + * adding them to the `$httpProvider.interceptors` array. The factory is called and + * injected with dependencies (if specified) and returns the interceptor. + * + * There are two kinds of interceptors (and two kinds of rejection interceptors): + * + * * `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` 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; + * }, + * + * // optional method + * 'requestError': function(rejection) { + * // do something on error + * if (canRecover(rejection)) { + * return responseOrNewPromise + * } + * return $q.reject(rejection); + * }, + * + * + * + * // optional method + * 'response': function(response) { + * // do something on success + * return response; + * }, + * + * // optional method + * 'responseError': function(rejection) { + * // do something on error + * if (canRecover(rejection)) { + * return responseOrNewPromise + * } + * return $q.reject(rejection); + * } + * }; + * }); + * + * $httpProvider.interceptors.push('myHttpInterceptor'); + * + * + * // alternatively, register the interceptor via an anonymous factory + * $httpProvider.interceptors.push(function($q, dependency1, dependency2) { + * return { + * 'request': function(config) { + * // same as above + * }, + * + * 'response': function(response) { + * // same as above + * } + * }; + * }); + * ``` + * + * ## Security Considerations + * + * When designing web applications, consider security threats from: + * + * - [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 + * + * 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 + * + * [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 + * server can verify that the cookie matches `X-XSRF-TOKEN` HTTP header, and therefore be sure + * 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 [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 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. 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, status)|Array.}` – + * transform function or an array of such functions. The transform function takes the http + * 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 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}` that will be resolved to a response object + * when the request succeeds or fails. + * + * + * @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}}
      +
      http response data: {{data}}
      +
      +
      + + angular.module('httpExample', []) + .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; + + $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; + }; + }]); + + + 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() { + sampleGetBtn.click(); + fetchBtn.click(); + expect(status.getText()).toMatch('200'); + expect(data.getText()).toMatch(/Hello, \$http!/); + }); + +// 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() { + invalidJsonpBtn.click(); + fetchBtn.click(); + expect(status.getText()).toMatch('0'); + expect(data.getText()).toMatch('Request failed'); + }); + +
      + */ + function $http(requestConfig) { + + if (!isObject(requestConfig)) { + throw minErr('$http')('badreq', 'Http request configuration must be an object. Received: {0}', requestConfig); + } + + if (!isString(requestConfig.url)) { + throw minErr('$http')('badreq', 'Http request configuration url must be a string. Received: {0}', requestConfig.url); + } + + var config = extend({ + method: 'get', + transformRequest: defaults.transformRequest, + transformResponse: defaults.transformResponse, + paramSerializer: defaults.paramSerializer + }, requestConfig); + + config.headers = mergeHeaders(requestConfig); + config.method = uppercase(config.method); + config.paramSerializer = isString(config.paramSerializer) ? + $injector.get(config.paramSerializer) : config.paramSerializer; + + var requestInterceptors = []; + var responseInterceptors = []; + var promise = $q.when(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); + + if (useLegacyPromise) { + promise.success = function(fn) { + assertArgFn(fn, 'fn'); + + promise.then(function(response) { + fn(response.data, response.status, response.headers, config); + }); + return promise; + }; + + promise.error = function(fn) { + assertArgFn(fn, 'fn'); + + promise.then(null, function(response) { + fn(response.data, response.status, response.headers, config); + }); + return promise; + }; + } else { + promise.success = $httpMinErrLegacyFn('success'); + promise.error = $httpMinErrLegacyFn('error'); + } + + 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 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(reqData)) { + forEach(headers, function(value, header) { + if (lowercase(header) === 'content-type') { + delete headers[header]; + } + }); + } + + if (isUndefined(config.withCredentials) && !isUndefined(defaults.withCredentials)) { + config.withCredentials = defaults.withCredentials; + } + + // send request + return sendReq(config, reqData).then(transformResponse, transformResponse); + } + + function transformResponse(response) { + // make a copy since the response must be cacheable + var resp = extend({}, response); + resp.data = transformData(response.data, response.headers, response.status, + config.transformResponse); + return (isSuccess(response.status)) + ? resp + : $q.reject(resp); + } + } + + $http.pendingRequests = []; + + /** + * @ngdoc method + * @name $http#get + * + * @description + * Shortcut method to perform `GET` request. + * + * @param {string} url Relative or absolute URL specifying the destination of the request + * @param {Object=} config Optional configuration object + * @returns {HttpPromise} Future object + */ + + /** + * @ngdoc method + * @name $http#delete + * + * @description + * Shortcut method to perform `DELETE` request. + * + * @param {string} url Relative or absolute URL specifying the destination of the request + * @param {Object=} config Optional configuration object + * @returns {HttpPromise} Future object + */ + + /** + * @ngdoc method + * @name $http#head + * + * @description + * Shortcut method to perform `HEAD` request. + * + * @param {string} url Relative or absolute URL specifying the destination of the request + * @param {Object=} config Optional configuration object + * @returns {HttpPromise} Future object + */ + + /** + * @ngdoc method + * @name $http#jsonp + * + * @description + * Shortcut method to perform `JSONP` request. + * If you would like to customize where and how the callbacks are stored then try overriding + * or decorating the {@link $jsonpCallbacks} service. + * + * @param {string} url Relative or absolute URL specifying the destination of the request. + * The name of the callback should be the string `JSON_CALLBACK`. + * @param {Object=} config Optional configuration object + * @returns {HttpPromise} Future object + */ + createShortMethods('get', 'delete', 'head', 'jsonp'); + + /** + * @ngdoc method + * @name $http#post + * + * @description + * Shortcut method to perform `POST` 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 + */ + + /** + * @ngdoc method + * @name $http#put + * + * @description + * Shortcut method to perform `PUT` 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 + */ + + /** + * @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 $http#defaults + * + * @description + * Runtime equivalent of the `$httpProvider.defaults` property. Allows configuration of + * default headers, withCredentials as well as request and response transformations. + * + * See "Setting HTTP Headers" and "Transforming Requests and Responses" sections above. + */ + $http.defaults = defaults; + + + return $http; + + + function createShortMethods(names) { + forEach(arguments, function(name) { + $http[name] = function(url, config) { + return $http(extend({}, config || {}, { + method: name, + url: url + })); + }; + }); + } + + + function createShortMethodsWithData(name) { + forEach(arguments, function(name) { + $http[name] = function(url, data, config) { + return $http(extend({}, config || {}, { + method: name, + url: url, + data: data + })); + }; + }); + } + + + /** + * Makes the request. + * + * !!! ACCESSES CLOSURE VARS: + * $httpBackend, defaults, $log, $rootScope, defaultCache, $http.pendingRequests + */ + function sendReq(config, reqData) { + var deferred = $q.defer(), + promise = deferred.promise, + cache, + cachedResp, + reqHeaders = config.headers, + url = buildUrl(config.url, config.paramSerializer(config.params)); + + $http.pendingRequests.push(config); + promise.then(removePendingReq, removePendingReq); + + + 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 + : defaultCache; + } + + if (cache) { + cachedResp = cache.get(url); + if (isDefined(cachedResp)) { + if (isPromiseLike(cachedResp)) { + // cached request has already been sent, but there is no response yet + cachedResp.then(resolvePromiseWithResult, resolvePromiseWithResult); + } else { + // serving from cache + if (isArray(cachedResp)) { + resolvePromise(cachedResp[1], cachedResp[0], shallowCopy(cachedResp[2]), cachedResp[3]); + } else { + resolvePromise(cachedResp, 200, {}, 'OK'); + } + } + } else { + // put the promise for the non-transformed response into cache as a placeholder + cache.put(url, promise); + } + } + + + // 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, + 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(): + * - caches the response if desired + * - resolves the raw $http promise + * - calls $apply + */ + function done(status, response, headersString, statusText) { + if (cache) { + if (isSuccess(status)) { + cache.put(url, [status, response, parseHeaders(headersString), statusText]); + } else { + // remove promise from the cache + cache.remove(url); + } + } + + function resolveHttpPromise() { + resolvePromise(response, status, headersString, statusText); + } + + if (useApplyAsync) { + $rootScope.$applyAsync(resolveHttpPromise); + } else { + resolveHttpPromise(); + if (!$rootScope.$$phase) $rootScope.$apply(); + } + } + + + /** + * Resolves the raw $http promise. + */ + function resolvePromise(response, status, headers, statusText) { + //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, + statusText: statusText + }); + } + + function resolvePromiseWithResult(result) { + resolvePromise(result.data, result.status, shallowCopy(result.headers()), result.statusText); + } + + function removePendingReq() { + var idx = $http.pendingRequests.indexOf(config); + if (idx !== -1) $http.pendingRequests.splice(idx, 1); + } + } + + + function buildUrl(url, serializedParams) { + if (serializedParams.length > 0) { + url += ((url.indexOf('?') === -1) ? '?' : '&') + serializedParams; + } + return url; + } + }]; +} + +/** + * @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 service + * @name $httpBackend + * @requires $jsonpCallbacks + * @requires $document + * @requires $xhrFactory + * @this + * + * @description + * HTTP backend used by the {@link ng.$http service} that delegates to + * XMLHttpRequest object or JSONP and deals with browser incompatibilities. + * + * You should never need to use this service directly, instead use the higher-level abstractions: + * {@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. + */ +function $HttpBackendProvider() { + 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) { + // TODO(vojta): fix the signature + return function(method, url, post, callback, headers, timeout, withCredentials, responseType, eventHandlers, uploadEventHandlers) { + $browser.$$incOutstandingRequestCount(); + url = url || $browser.url(); + + 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); + callbacks.removeCallback(callbackPath); + }); + } else { + + var xhr = createXhr(method, url); + + xhr.open(method, url, true); + forEach(headers, function(value, key) { + if (isDefined(value)) { + xhr.setRequestHeader(key, value); + } + }); + + xhr.onload = function requestLoaded() { + var statusText = xhr.statusText || ''; + + // 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; + + // normalize IE9 bug (http://bugs.jquery.com/ticket/1450) + var status = xhr.status === 1223 ? 204 : xhr.status; + + // 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); + }; + + 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, ''); + }; + + xhr.onerror = requestError; + xhr.onabort = requestError; + xhr.ontimeout = requestError; + + 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) { + 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(isUndefined(post) ? null : post); + } + + if (timeout > 0) { + var timeoutId = $browserDefer(timeoutRequest, timeout); + } else if (isPromiseLike(timeout)) { + timeout.then(timeoutRequest); + } + + + function timeoutRequest() { + if (jsonpDone) { + jsonpDone(); + } + if (xhr) { + xhr.abort(); + } + } + + function completeRequest(callback, status, response, headersString, statusText) { + // cancel timeout and subsequent timeout promise resolution + if (isDefined(timeoutId)) { + $browserDefer.cancel(timeoutId); + } + jsonpDone = xhr = null; + + callback(status, response, headersString, statusText); + $browser.$$completeOutstandingRequest(noop); + } + }; + + 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'), callback = null; + script.type = 'text/javascript'; + script.src = url; + script.async = true; + + callback = function(event) { + removeEventListenerFn(script, 'load', callback); + removeEventListenerFn(script, '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' }; + } + text = event.type; + status = event.type === 'error' ? 404 : 200; + } + + if (done) { + done(status, text); + } + }; + + addEventListenerFn(script, 'load', callback); + addEventListenerFn(script, 'error', callback); + rawDocument.body.appendChild(script); + return callback; + } +} + +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 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(element(by.binding('demo.label')).getText()).toBe('This binding is brought you by // interpolation symbols.'); + }); + +
      + */ +function $InterpolateProvider() { + var startSymbol = '{{'; + var endSymbol = '}}'; + + /** + * @ngdoc method + * @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. + * @returns {string|self} Returns the symbol when used as getter and self if used as setter. + */ + this.startSymbol = function(value) { + if (value) { + startSymbol = value; + return this; + } else { + return startSymbol; + } + }; + + /** + * @ngdoc method + * @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) { + if (value) { + endSymbol = value; + return this; + } else { + return endSymbol; + } + }; + + + this.$get = ['$parse', '$exceptionHandler', '$sce', function($parse, $exceptionHandler, $sce) { + var startSymbolLength = startSymbol.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); + } + + function stringify(value) { + if (value == null) { // null || undefined + return ''; + } + switch (typeof value) { + case 'string': + break; + case 'number': + value = '' + value; + break; + default: + value = toJson(value); + } + + return value; + } + + // 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 service + * @name $interpolate + * @kind function + * + * @requires $parse + * @requires $sce + * + * @description + * + * Compiles a string with markup into an interpolation function. This service is used by the + * HTML {@link ng.$compile $compile} service for data binding. See + * {@link ng.$interpolateProvider $interpolateProvider} for configuring the + * interpolation markup. + * + * + * ```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#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`: evaluation context for all expressions embedded in the interpolated text + */ + 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; + } + + 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 + // concatenated values are unsafe to use and could easily lead to XSS. By requiring that a + // single expression be used for iframe[src], object[src], etc., we ensure that the value + // 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 && concat.length > 1) { + $interpolateMinErr.throwNoconcat(text); + } + + if (!mustHaveExpression || expressions.length) { + var compute = function(values) { + for (var i = 0, ii = expressions.length; i < ii; i++) { + if (allOrNothing && isUndefined(values[i])) return; + concat[expressionPositions[i]] = values[i]; + } + return concat.join(''); + }; + + var getValue = function(value) { + return trustedContext ? + $sce.getTrusted(trustedContext, value) : + $sce.valueOf(value); + }; + + return extend(function interpolationFn(context) { + var i = 0; + var ii = expressions.length; + var values = new Array(ii); + + try { + for (; i < ii; i++) { + values[i] = parseFns[i](context); + } + + return compute(values); + } catch (err) { + $exceptionHandler($interpolateMinErr.interr(text, err)); + } + + }, { + // all of these properties are undocumented for now + exp: text, //just for compatibility with regular watchers created via $watch + expressions: expressions, + $$watchDelegate: function(scope, listener) { + var lastValue; + return scope.$watchGroup(parseFns, /** @this */ function interpolateFnWatcher(values, oldValues) { + var currValue = compute(values); + if (isFunction(listener)) { + listener.call(this, currValue, values !== oldValues ? lastValue : currValue, scope); + } + lastValue = currValue; + }); + } + }); + } + + function parseStringifyInterceptor(value) { + try { + value = getValue(value); + return allOrNothing && !isDefined(value) ? value : stringify(value); + } catch (err) { + $exceptionHandler($interpolateMinErr.interr(text, err)); + } + } + } + + + /** + * @ngdoc method + * @name $interpolate#startSymbol + * @description + * Symbol to denote the start of expression in the interpolated string. Defaults to `{{`. + * + * Use {@link ng.$interpolateProvider#startSymbol `$interpolateProvider.startSymbol`} to change + * the symbol. + * + * @returns {string} start symbol. + */ + $interpolate.startSymbol = function() { + return startSymbol; + }; + + + /** + * @ngdoc method + * @name $interpolate#endSymbol + * @description + * Symbol to denote the end of expression in the interpolated string. Defaults to `}}`. + * + * Use {@link ng.$interpolateProvider#endSymbol `$interpolateProvider.endSymbol`} to change + * the symbol. + * + * @returns {string} end symbol. + */ + $interpolate.endSymbol = function() { + return endSymbol; + }; + + return $interpolate; + }]; +} + +/** @this */ +function $IntervalProvider() { + this.$get = ['$rootScope', '$window', '$q', '$$q', '$browser', + function($rootScope, $window, $q, $$q, $browser) { + var intervals = {}; + + + /** + * @ngdoc service + * @name $interval + * + * @description + * Angular's wrapper for `window.setInterval`. The `fn` function is executed every `delay` + * milliseconds. + * + * The return value of registering an interval function is a promise. This promise will be + * notified upon each tick of the interval, and will be resolved after `count` iterations, or + * run indefinitely if `count` is not defined. The value of the notification will be the + * number of iterations that have run. + * To cancel an interval, call `$interval.cancel(promise)`. + * + * In tests you can use {@link ngMock.$interval#flush `$interval.flush(millis)`} to + * move forward by `millis` milliseconds and trigger any functions scheduled to run in that + * time. + * + *
      + * **Note**: Intervals created by this service must be explicitly destroyed when you are finished + * with them. In particular they are not automatically destroyed when a controller's scope or a + * directive's element are destroyed. + * You should take this into consideration and make sure to always cancel the interval at the + * appropriate moment. See the example below for more details on how and when to do this. + *
      + * + * @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#$apply $apply} block. + * @param {...*=} Pass additional parameters to the executed function. + * @returns {promise} A promise which will be notified on each iteration. + * + * @example + * + * + * + * + *
      + *
      + *
      + * Current time is: + *
      + * Blood 1 : {{blood_1}} + * Blood 2 : {{blood_2}} + * + * + * + *
      + *
      + * + *
      + *
      + */ + function interval(fn, delay, count, 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.$$intervalId = setInterval(function tick() { + if (skipApply) { + $browser.defer(callback); + } else { + $rootScope.$evalAsync(callback); + } + deferred.notify(iteration++); + + if (count > 0 && iteration >= count) { + deferred.resolve(iteration); + clearInterval(promise.$$intervalId); + delete intervals[promise.$$intervalId]; + } + + if (!skipApply) $rootScope.$apply(); + + }, delay); + + intervals[promise.$$intervalId] = deferred; + + return promise; + + function callback() { + if (!hasParams) { + fn(iteration); + } else { + fn.apply(null, args); + } + } + } + + + /** + * @ngdoc method + * @name $interval#cancel + * + * @description + * Cancels a task associated with the `promise`. + * + * @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) { + intervals[promise.$$intervalId].reject('canceled'); + $window.clearInterval(promise.$$intervalId); + delete intervals[promise.$$intervalId]; + return true; + } + return false; + }; + + return interval; + }]; +} + +/** + * @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 = ['$window', function($window) { + var callbacks = $window.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 + * only public api is: + * + * * `id` – `{string}` – locale id formatted as `languageId-countryId` (e.g. `en-us`) + */ + +var PATH_MATCH = /^([^?#]*)(\?([^#]*))?(#(.*))?$/, + DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp': 21}; +var $locationMinErr = minErr('$location'); + + +/** + * Encode path using encodeUriSegment, ignoring forward slashes + * + * @param {string} path Path to encode + * @returns {string} + */ +function encodePath(path) { + var segments = path.split('/'), + i = segments.length; + + while (i--) { + segments[i] = encodeUriSegment(segments[i]); + } + + return segments.join('/'); +} + +function parseAbsoluteUrl(absoluteUrl, locationObj) { + var parsedUrl = urlResolve(absoluteUrl); + + locationObj.$$protocol = parsedUrl.protocol; + locationObj.$$host = parsedUrl.hostname; + locationObj.$$port = toInt(parsedUrl.port) || DEFAULT_PORTS[parsedUrl.protocol] || null; +} + +var DOUBLE_SLASH_REGEX = /^\s*[\\/]{2,}/; +function parseAppUrl(url, locationObj) { + + if (DOUBLE_SLASH_REGEX.test(url)) { + throw $locationMinErr('badpath', 'Invalid url "{0}".', url); + } + + 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); + locationObj.$$search = parseKeyValue(match.search); + locationObj.$$hash = decodeURIComponent(match.hash); + + // make sure path starts with '/'; + if (locationObj.$$path && locationObj.$$path.charAt(0) !== '/') { + locationObj.$$path = '/' + locationObj.$$path; + } +} + +function startsWith(str, search) { + return str.slice(0, search.length) === search; +} + +/** + * + * @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 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); +} + +function trimEmptyHash(url) { + return url.replace(/(#.+)|#$/, '$1'); +} + + +function stripFile(url) { + return url.substr(0, stripHash(url).lastIndexOf('/') + 1); +} + +/* return the server only (scheme://host:port) */ +function serverBase(url) { + return url.substring(0, url.indexOf('/', url.indexOf('//') + 2)); +} + + +/** + * 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} appBaseNoFile application base URL stripped of any filename + * @param {string} basePrefix URL path prefix + */ +function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) { + this.$$html5 = true; + basePrefix = basePrefix || ''; + parseAbsoluteUrl(appBase, this); + + + /** + * Parse given HTML5 (regular) URL string into properties + * @param {string} url HTML5 URL + * @private + */ + this.$$parse = function(url) { + var pathUrl = stripBaseUrl(appBaseNoFile, url); + if (!isString(pathUrl)) { + throw $locationMinErr('ipthprfx', 'Invalid url "{0}", missing path prefix "{1}".', url, + appBaseNoFile); + } + + parseAppUrl(pathUrl, this); + + if (!this.$$path) { + this.$$path = '/'; + } + + this.$$compose(); + }; + + /** + * Compose url and update `absUrl` property + * @private + */ + this.$$compose = function() { + var search = toKeyValue(this.$$search), + 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.$$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 + * 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, appBaseNoFile, hashPrefix) { + + parseAbsoluteUrl(appBase, this); + + + /** + * Parse given hashbang URL into properties + * @param {string} url Hashbang URL + * @private + */ + this.$$parse = function(url) { + var withoutBaseUrl = stripBaseUrl(appBase, url) || stripBaseUrl(appBaseNoFile, url); + var withoutHashUrl; + + 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; + this.replace(); + } + } + } + + 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) { + /* + 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 (startsWith(url, base)) { + url = url.replace(base, ''); + } + + // The input URL intentionally contains a first path segment that ends with a colon. + if (windowsFilePathExp.exec(url)) { + return path; + } + + firstPathSegmentMatch = windowsFilePathExp.exec(path); + return firstPathSegmentMatch ? firstPathSegmentMatch[1] : path; + } + }; + + /** + * Compose hashbang URL and update `absUrl` property + * @private + */ + this.$$compose = function() { + var search = toKeyValue(this.$$search), + hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : ''; + + this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; + this.$$absUrl = appBase + (this.$$url ? hashPrefix + this.$$url : ''); + }; + + this.$$parseLinkUrl = function(url, relHref) { + if (stripHash(appBase) === stripHash(url)) { + this.$$parse(url); + return true; + } + return false; + }; +} + + +/** + * 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, appBaseNoFile, hashPrefix) { + this.$$html5 = true; + LocationHashbangUrl.apply(this, arguments); + + 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 rewrittenUrl; + var appUrl; + + 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; + }; + +} + + +var locationPrototype = { + + /** + * Ensure absolute URL is initialized. + * @private + */ + $$absUrl:'', + + /** + * Are we in html5 mode? + * @private + */ + $$html5: false, + + /** + * Has any change been replacing? + * @private + */ + $$replace: false, + + /** + * @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'), + + /** + * @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; + } + + 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] || ''); + + return this; + }, + + /** + * @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 $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 $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 $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; + }), + + /** + * @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]; + }); + + 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; + + return this; + }; +}); + + +function locationGetter(property) { + return /** @this */ function() { + return this[property]; + }; +} + + +function locationGetterSetter(property, preprocess) { + return /** @this */ function(value) { + if (isUndefined(value)) { + return this[property]; + } + + this[property] = preprocess(value); + this.$$compose(); + + return this; + }; +} + + +/** + * @ngdoc service + * @name $location + * + * @requires $rootElement + * + * @description + * The $location service parses the URL in the browser address bar (based on the + * [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. + * + * **The $location service:** + * + * - Exposes the current URL in the browser address bar, so you can + * - Watch and observe the URL. + * - Change the URL. + * - Synchronizes the URL with the browser when the user + * - Changes the address bar. + * - Clicks the back or forward button (or clicks a History link). + * - 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/$location Developer Guide: Using $location} + */ + +/** + * @ngdoc provider + * @name $locationProvider + * @this + * + * @description + * Use the `$locationProvider` to configure how the application deep linking paths are stored. + */ +function $LocationProvider() { + var hashPrefix = '', + html5Mode = { + enabled: false, + requireBase: true, + rewriteLinks: true + }; + + /** + * @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 + */ + this.hashPrefix = function(prefix) { + if (isDefined(prefix)) { + hashPrefix = prefix; + return this; + } else { + return hashPrefix; + } + }; + + /** + * @ngdoc method + * @name $locationProvider#html5Mode + * @description + * @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 (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; + } + }; + + /** + * @ngdoc event + * @name $location#$locationChangeStart + * @eventType broadcast on root scope + * @description + * 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#$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 $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', '$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; + + 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; + } + 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 (!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 (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 + // an animation. + absHref = urlResolve(absHref.animVal).href; + } + + // Ignore when url is started with javascript: or mailto: + if (IGNORE_URI_REGEXP.test(absHref)) return; + + 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 + 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 (trimEmptyHash($location.absUrl()) !== trimEmptyHash(initialUrl)) { + $browser.url($location.absUrl(), true); + } + + var initializing = true; + + // update $location when $browser url changes + $browser.onUrlChange(function(newUrl, newState) { + + if (isUndefined(stripBaseUrl(appBaseNoFile, newUrl))) { + // 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 + $rootScope.$watch(function $locationWatch() { + 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; + + // 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, oldState) { + $rootScope.$broadcast('$locationChangeSuccess', $location.absUrl(), oldUrl, + $location.$$state, oldState); + } +}]; +} + +/** + * @ngdoc service + * @name $log + * @requires $window + * + * @description + * Simple service for logging. Default implementation safely writes the message + * into the browser's console (if present). + * + * The main purpose of this service is to simplify debugging and troubleshooting. + * + * The default is to log `debug` messages. You can use + * {@link ng.$logProvider ng.$logProvider#debugEnabled} to change this. + * + * @example + + + 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...

      + + + + + + +
      +
      +
      + */ + +/** + * @ngdoc provider + * @name $logProvider + * @this + * + * @description + * Use the `$logProvider` to configure how the application logs messages + */ +function $LogProvider() { + var debug = true, + self = this; + + /** + * @ngdoc method + * @name $logProvider#debugEnabled + * @description + * @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) { + if (isDefined(flag)) { + debug = flag; + return this; + } else { + return debug; + } + }; + + this.$get = ['$window', function($window) { + return { + /** + * @ngdoc method + * @name $log#log + * + * @description + * Write a log message + */ + log: consoleLog('log'), + + /** + * @ngdoc method + * @name $log#info + * + * @description + * Write an information message + */ + info: consoleLog('info'), + + /** + * @ngdoc method + * @name $log#warn + * + * @description + * Write a warning message + */ + warn: consoleLog('warn'), + + /** + * @ngdoc method + * @name $log#error + * + * @description + * Write an error message + */ + error: consoleLog('error'), + + /** + * @ngdoc method + * @name $log#debug + * + * @description + * Write a debug message + */ + debug: (function() { + var fn = consoleLog('debug'); + + return function() { + if (debug) { + fn.apply(self, arguments); + } + }; + })() + }; + + function formatError(arg) { + if (arg instanceof Error) { + if (arg.stack) { + arg = (arg.message && arg.stack.indexOf(arg.message) === -1) + ? 'Error: ' + arg.message + '\n' + arg.stack + : arg.stack; + } else if (arg.sourceURL) { + arg = arg.message + '\n' + arg.sourceURL + ':' + arg.line; + } + } + return arg; + } + + function consoleLog(type) { + var console = $window.console || {}, + logFn = console[type] || console.log || noop, + hasApply = false; + + // 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) { /* empty */ } + + 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); + }; + } + }]; +} + +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * 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 ARRAY_CTOR = [].constructor; +var BOOLEAN_CTOR = (false).constructor; +var FUNCTION_CTOR = Function.constructor; +var NUMBER_CTOR = (0).constructor; +var OBJECT_CTOR = {}.constructor; +var STRING_CTOR = ''.constructor; +var ARRAY_CTOR_PROTO = ARRAY_CTOR.prototype; +var BOOLEAN_CTOR_PROTO = BOOLEAN_CTOR.prototype; +var FUNCTION_CTOR_PROTO = FUNCTION_CTOR.prototype; +var NUMBER_CTOR_PROTO = NUMBER_CTOR.prototype; +var OBJECT_CTOR_PROTO = OBJECT_CTOR.prototype; +var STRING_CTOR_PROTO = STRING_CTOR.prototype; + +var CALL = FUNCTION_CTOR_PROTO.call; +var APPLY = FUNCTION_CTOR_PROTO.apply; +var BIND = FUNCTION_CTOR_PROTO.bind; + +var objectValueOf = OBJECT_CTOR_PROTO.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. +// +// As an example, consider the following Angular expression: +// +// {}.toString.constructor('alert("evil JS code")') +// +// 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. +// +// 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. +// Similarly we prevent invocations of function known to be dangerous, as well as assignments to +// native objects. +// +// See https://docs.angularjs.org/guide/security + + +function ensureSafeMemberName(name, fullExpression) { + if (name === '__defineGetter__' || name === '__defineSetter__' + || name === '__lookupGetter__' || name === '__lookupSetter__' + || name === '__proto__') { + throw $parseMinErr('isecfld', + 'Attempting to access a disallowed field in Angular expressions! ' + + '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.window === obj) { + throw $parseMinErr('isecwindow', + 'Referencing the Window in Angular expressions is disallowed! Expression: {0}', + fullExpression); + } else if (// isElement(obj) + obj.children && (obj.nodeName || (obj.prop && obj.attr && obj.find))) { + throw $parseMinErr('isecdom', + 'Referencing DOM nodes in Angular expressions is disallowed! Expression: {0}', + fullExpression); + } else if (// block Object so that we can't get hold of dangerous Object.* methods + obj === Object) { + throw $parseMinErr('isecobj', + 'Referencing Object in Angular expressions is disallowed! Expression: {0}', + fullExpression); + } + } + return obj; +} + +function ensureSafeFunction(obj, fullExpression) { + if (obj) { + if (obj.constructor === obj) { + throw $parseMinErr('isecfn', + 'Referencing Function in Angular expressions is disallowed! Expression: {0}', + fullExpression); + } else if (obj === CALL || obj === APPLY || obj === BIND) { + throw $parseMinErr('isecff', + 'Referencing call, apply or bind in Angular expressions is disallowed! Expression: {0}', + fullExpression); + } + } +} + +function ensureSafeAssignContext(obj, fullExpression) { + if (obj) { + if (obj === ARRAY_CTOR || + obj === BOOLEAN_CTOR || + obj === FUNCTION_CTOR || + obj === NUMBER_CTOR || + obj === OBJECT_CTOR || + obj === STRING_CTOR || + obj === ARRAY_CTOR_PROTO || + obj === BOOLEAN_CTOR_PROTO || + obj === FUNCTION_CTOR_PROTO || + obj === NUMBER_CTOR_PROTO || + obj === OBJECT_CTOR_PROTO || + obj === STRING_CTOR_PROTO) { + throw $parseMinErr('isecaf', + 'Assigning to a constructor or its prototype is disallowed! Expression: {0}', + fullExpression); + } + } +} + +var OPERATORS = createMap(); +forEach('+ - * / % === !== == != < > <= >= && || ! = |'.split(' '), function(operator) { OPERATORS[operator] = true; }); +var ESCAPE = {'n':'\n', 'f':'\f', 'r':'\r', 't':'\t', 'v':'\v', '\'':'\'', '"':'"'}; + + +///////////////////////////////////////// + + +/** + * @constructor + */ +var Lexer = function Lexer(options) { + this.options = options; +}; + +Lexer.prototype = { + constructor: Lexer, + + lex: function(text) { + this.text = text; + this.index = 0; + this.tokens = []; + + while (this.index < this.text.length) { + 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.isIdentifierStart(this.peekMultichar())) { + this.readIdent(); + } else if (this.is(ch, '(){}[].,;:?')) { + this.tokens.push({index: this.index, text: ch}); + this.index++; + } else if (this.isWhitespace(ch)) { + this.index++; + } else { + var ch2 = ch + this.peek(); + var ch3 = ch2 + this.peek(2); + 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); + } + } + } + return this.tokens; + }, + + is: function(ch, chars) { + return chars.indexOf(ch) !== -1; + }, + + peek: function(i) { + var num = i || 1; + return (this.index + num < this.text.length) ? this.text.charAt(this.index + num) : false; + }, + + isNumber: function(ch) { + 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'); + }, + + 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 === '$'); + }, + + 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) { + return (ch === '-' || ch === '+' || this.isNumber(ch)); + }, + + throwError: function(error, start, end) { + end = end || this.index; + var colStr = (isDefined(start) + ? '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); + }, + + readNumber: function() { + var number = ''; + var start = this.index; + while (this.index < this.text.length) { + var ch = lowercase(this.text.charAt(this.index)); + if (ch === '.' || this.isNumber(ch)) { + number += ch; + } else { + var peekCh = this.peek(); + if (ch === 'e' && this.isExpOperator(peekCh)) { + number += ch; + } else if (this.isExpOperator(ch) && + 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') { + this.throwError('Invalid exponent'); + } else { + break; + } + } + this.index++; + } + this.tokens.push({ + index: start, + text: number, + constant: true, + value: Number(number) + }); + }, + + readIdent: function() { + var start = this.index; + this.index += this.peekMultichar().length; + while (this.index < this.text.length) { + var ch = this.peekMultichar(); + if (!this.isIdentifierContinue(ch)) { + break; + } + this.index += ch.length; + } + this.tokens.push({ + index: start, + text: this.text.slice(start, this.index), + identifier: true + }); + }, + + readString: function(quote) { + var start = this.index; + this.index++; + var string = ''; + var rawString = quote; + var escape = false; + while (this.index < this.text.length) { + var ch = this.text.charAt(this.index); + rawString += ch; + if (escape) { + if (ch === 'u') { + var hex = this.text.substring(this.index + 1, this.index + 5); + 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]; + string = string + (rep || ch); + } + escape = false; + } else if (ch === '\\') { + escape = true; + } else if (ch === quote) { + this.index++; + this.tokens.push({ + index: start, + text: rawString, + constant: true, + value: string + }); + return; + } else { + string += ch; + } + this.index++; + } + this.throwError('Unterminated quote', start); + } +}; + +var AST = function AST(lexer, options) { + this.lexer = lexer; + this.options = options; +}; + +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'; + +// Internal use only +AST.NGValueParameter = 'NGValueParameter'; + +AST.prototype = { + ast: function(text) { + this.text = text; + this.tokens = this.lexer.lex(text); + + var value = this.program(); + + if (this.tokens.length !== 0) { + this.throwError('is an unexpected token', this.tokens[0]); + } + + return value; + }, + + program: function() { + var body = []; + while (true) { + if (this.tokens.length > 0 && !this.peek('}', ')', ';', ']')) + body.push(this.expressionStatement()); + if (!this.expect(';')) { + return { type: AST.Program, body: body}; + } + } + }, + + expressionStatement: function() { + return { type: AST.ExpressionStatement, expression: this.filterChain() }; + }, + + filterChain: function() { + var left = this.expression(); + while (this.expect('|')) { + left = this.filter(left); + } + return left; + }, + + expression: function() { + return this.assignment(); + }, + + assignment: function() { + var result = this.ternary(); + if (this.expect('=')) { + if (!isAssignable(result)) { + throw $parseMinErr('lval', 'Trying to assign a value to a non l-value'); + } + + result = { type: AST.AssignmentExpression, left: result, right: this.assignment(), operator: '='}; + } + return result; + }, + + ternary: function() { + 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}; + } + } + return test; + }, + + logicalOR: function() { + var left = this.logicalAND(); + while (this.expect('||')) { + left = { type: AST.LogicalExpression, operator: '||', left: left, right: this.logicalAND() }; + } + return left; + }, + + logicalAND: function() { + var left = this.equality(); + while (this.expect('&&')) { + left = { type: AST.LogicalExpression, operator: '&&', left: left, right: this.equality()}; + } + return left; + }, + + equality: function() { + var left = this.relational(); + var token; + while ((token = this.expect('==','!=','===','!=='))) { + left = { type: AST.BinaryExpression, operator: token.text, left: left, right: this.relational() }; + } + return left; + }, + + relational: function() { + var left = this.additive(); + var token; + while ((token = this.expect('<', '>', '<=', '>='))) { + left = { type: AST.BinaryExpression, operator: token.text, left: left, right: this.additive() }; + } + return left; + }, + + additive: function() { + var left = this.multiplicative(); + var token; + while ((token = this.expect('+','-'))) { + left = { type: AST.BinaryExpression, operator: token.text, left: left, right: this.multiplicative() }; + } + return left; + }, + + multiplicative: function() { + var left = this.unary(); + var token; + while ((token = this.expect('*','/','%'))) { + left = { type: AST.BinaryExpression, operator: token.text, left: left, right: this.unary() }; + } + return left; + }, + + unary: function() { + var token; + if ((token = this.expect('+', '-', '!'))) { + return { type: AST.UnaryExpression, operator: token.text, prefix: true, argument: this.unary() }; + } else { + return this.primary(); + } + }, + + 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()); + } + + 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; + }, + + filter: function(baseExpression) { + var args = [baseExpression]; + var result = {type: AST.CallExpression, callee: this.identifier(), arguments: args, filter: true}; + + while (this.expect(':')) { + args.push(this.expression()); + } + + return result; + }, + + parseArguments: function() { + var args = []; + if (this.peekToken().text !== ')') { + do { + args.push(this.filterChain()); + } while (this.expect(',')); + } + return args; + }, + + 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 { + if (this.peek(']')) { + // Support trailing commas per ES5.1. + break; + } + elements.push(this.expression()); + } while (this.expect(',')); + } + this.consume(']'); + + return { type: AST.ArrayExpression, elements: elements }; + }, + + object: function() { + var properties = [], property; + if (this.peekToken().text !== '}') { + do { + 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 {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 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; +} + +function findConstantAndWatchExpressions(ast, $filter) { + var allConstants; + var argsToWatch; + var isStatelessFilter; + switch (ast.type) { + case AST.Program: + allConstants = true; + forEach(ast.body, function(expr) { + findConstantAndWatchExpressions(expr.expression, $filter); + 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); + ast.constant = ast.argument.constant; + ast.toWatch = ast.argument.toWatch; + break; + case AST.BinaryExpression: + findConstantAndWatchExpressions(ast.left, $filter); + findConstantAndWatchExpressions(ast.right, $filter); + 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); + findConstantAndWatchExpressions(ast.right, $filter); + ast.constant = ast.left.constant && ast.right.constant; + ast.toWatch = ast.constant ? [] : [ast]; + break; + case AST.ConditionalExpression: + findConstantAndWatchExpressions(ast.test, $filter); + findConstantAndWatchExpressions(ast.alternate, $filter); + findConstantAndWatchExpressions(ast.consequent, $filter); + 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); + if (ast.computed) { + findConstantAndWatchExpressions(ast.property, $filter); + } + ast.constant = ast.object.constant && (!ast.computed || ast.property.constant); + ast.toWatch = [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); + allConstants = allConstants && expr.constant; + if (!expr.constant) { + argsToWatch.push.apply(argsToWatch, expr.toWatch); + } + }); + ast.constant = allConstants; + ast.toWatch = isStatelessFilter ? argsToWatch : [ast]; + break; + case AST.AssignmentExpression: + findConstantAndWatchExpressions(ast.left, $filter); + findConstantAndWatchExpressions(ast.right, $filter); + 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); + allConstants = allConstants && expr.constant; + if (!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); + allConstants = allConstants && property.value.constant && !property.computed; + if (!property.value.constant) { + argsToWatch.push.apply(argsToWatch, property.value.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(astBuilder, $filter) { + this.astBuilder = astBuilder; + this.$filter = $filter; +} + +ASTCompiler.prototype = { + compile: function(expression, expensiveChecks) { + var self = this; + var ast = this.astBuilder.ast(expression); + this.state = { + nextId: 0, + filters: {}, + expensiveChecks: expensiveChecks, + 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(fnKey); + 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', + 'ensureSafeMemberName', + 'ensureSafeObject', + 'ensureSafeFunction', + 'getStringValue', + 'ensureSafeAssignContext', + 'ifDefined', + 'plus', + 'text', + fnString))( + this.$filter, + ensureSafeMemberName, + ensureSafeObject, + ensureSafeFunction, + getStringValue, + ensureSafeAssignContext, + ifDefined, + plusFn, + expression); + this.state = this.stage = undefined; + fn.literal = isLiteral(ast); + fn.constant = isConstant(ast); + return fn; + }, + + USE: 'use', + + STRICT: 'strict', + + watchFns: function() { + var result = []; + var fns = this.state.inputs; + var self = this; + forEach(fns, function(name) { + result.push('var ' + name + '=' + self.generateFunction(name, 's')); + }); + if (fns.length) { + result.push('fn.inputs=[' + fns.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(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; + } + ensureSafeMemberName(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.not(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)) + ); + if (self.state.expensiveChecks || isPossiblyDangerousMemberName(ast.name)) { + self.addEnsureSafeObject(intoId); + } + 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 (create && create !== 1) { + self.addEnsureSafeAssignContext(left); + } + if (ast.computed) { + right = self.nextId(); + self.recurse(ast.property, right); + self.getStringValue(right); + self.addEnsureSafeMemberName(right); + if (create && create !== 1) { + self.if_(self.not(self.computedMember(left, right)), self.lazyAssign(self.computedMember(left, right), '{}')); + } + expression = self.ensureSafeObject(self.computedMember(left, right)); + self.assign(intoId, expression); + if (nameId) { + nameId.computed = true; + nameId.name = right; + } + } else { + ensureSafeMemberName(ast.property.name); + if (create && create !== 1) { + self.if_(self.not(self.nonComputedMember(left, ast.property.name)), self.lazyAssign(self.nonComputedMember(left, ast.property.name), '{}')); + } + expression = self.nonComputedMember(left, ast.property.name); + if (self.state.expensiveChecks || isPossiblyDangerousMemberName(ast.property.name)) { + expression = self.ensureSafeObject(expression); + } + 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() { + self.addEnsureSafeFunction(right); + forEach(ast.arguments, function(expr) { + self.recurse(expr, self.nextId(), undefined, function(argument) { + args.push(self.ensureSafeObject(argument)); + }); + }); + if (left.name) { + if (!self.state.expensiveChecks) { + self.addEnsureSafeObject(left.context); + } + expression = self.member(left.context, left.name, left.computed) + '(' + args.join(',') + ')'; + } else { + expression = right + '(' + args.join(',') + ')'; + } + expression = self.ensureSafeObject(expression); + 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); + self.addEnsureSafeObject(self.member(left.context, left.name, left.computed)); + self.addEnsureSafeAssignContext(left.context); + 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, self.nextId(), undefined, function(argument) { + args.push(argument); + }); + }); + expression = '[' + args.join(',') + ']'; + this.assign(intoId, expression); + recursionFn(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('s'); + break; + case AST.LocalsExpression: + this.assign(intoId, 'l'); + recursionFn('l'); + break; + case AST.NGValueParameter: + this.assign(intoId, 'v'); + recursionFn('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 + ')'; + }, + + 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); + }, + + addEnsureSafeObject: function(item) { + this.current().body.push(this.ensureSafeObject(item), ';'); + }, + + addEnsureSafeMemberName: function(item) { + this.current().body.push(this.ensureSafeMemberName(item), ';'); + }, + + addEnsureSafeFunction: function(item) { + this.current().body.push(this.ensureSafeFunction(item), ';'); + }, + + addEnsureSafeAssignContext: function(item) { + this.current().body.push(this.ensureSafeAssignContext(item), ';'); + }, + + ensureSafeObject: function(item) { + return 'ensureSafeObject(' + item + ',text)'; + }, + + ensureSafeMemberName: function(item) { + return 'ensureSafeMemberName(' + item + ',text)'; + }, + + ensureSafeFunction: function(item) { + return 'ensureSafeFunction(' + item + ',text)'; + }, + + getStringValue: function(item) { + this.assign(item, 'getStringValue(' + item + ')'); + }, + + ensureSafeAssignContext: function(item) { + return 'ensureSafeAssignContext(' + item + ',text)'; + }, + + 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]; + } +}; + + +function ASTInterpreter(astBuilder, $filter) { + this.astBuilder = astBuilder; + this.$filter = $filter; +} + +ASTInterpreter.prototype = { + compile: function(expression, expensiveChecks) { + var self = this; + var ast = this.astBuilder.ast(expression); + this.expression = expression; + this.expensiveChecks = expensiveChecks; + findConstantAndWatchExpressions(ast, self.$filter); + var assignable; + var assign; + if ((assignable = assignableAST(ast))) { + assign = this.recurse(assignable); + } + var toWatch = getInputs(ast.body); + var inputs; + if (toWatch) { + inputs = []; + forEach(toWatch, function(watch, key) { + var input = self.recurse(watch); + watch.input = input; + inputs.push(input); + watch.watchId = key; + }); + } + 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); + }; + } + if (inputs) { + fn.inputs = inputs; + } + fn.literal = isLiteral(ast); + fn.constant = isConstant(ast); + return fn; + }, + + 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: + ensureSafeMemberName(ast.name, self.expression); + return self.identifier(ast.name, + self.expensiveChecks || isPossiblyDangerousMemberName(ast.name), + context, create, self.expression); + case AST.MemberExpression: + left = this.recurse(ast.object, false, !!create); + if (!ast.computed) { + ensureSafeMemberName(ast.property.name, self.expression); + right = ast.property.name; + } + if (ast.computed) right = this.recurse(ast.property); + return ast.computed ? + this.computedMember(left, right, context, create, self.expression) : + this.nonComputedMember(left, right, self.expensiveChecks, context, create, self.expression); + 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) { + ensureSafeObject(rhs.context, self.expression); + ensureSafeFunction(rhs.value, self.expression); + var values = []; + for (var i = 0; i < args.length; ++i) { + values.push(ensureSafeObject(args[i](scope, locals, assign, inputs), self.expression)); + } + value = ensureSafeObject(rhs.value.apply(rhs.context, values), self.expression); + } + 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); + ensureSafeObject(lhs.value, self.expression); + ensureSafeAssignContext(lhs.context); + 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; + }; + } + }, + + '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, expensiveChecks, context, create, expression) { + return function(scope, locals, assign, inputs) { + var base = locals && (name in locals) ? locals : scope; + if (create && create !== 1 && base && !(base[name])) { + base[name] = {}; + } + var value = base ? base[name] : undefined; + if (expensiveChecks) { + ensureSafeObject(value, expression); + } + if (context) { + return {context: base, name: name, value: value}; + } else { + return value; + } + }; + }, + computedMember: function(left, right, context, create, expression) { + 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); + ensureSafeMemberName(rhs, expression); + if (create && create !== 1) { + ensureSafeAssignContext(lhs); + if (lhs && !(lhs[rhs])) { + lhs[rhs] = {}; + } + } + value = lhs[rhs]; + ensureSafeObject(value, expression); + } + if (context) { + return {context: lhs, name: rhs, value: value}; + } else { + return value; + } + }; + }, + nonComputedMember: function(left, right, expensiveChecks, context, create, expression) { + return function(scope, locals, assign, inputs) { + var lhs = left(scope, locals, assign, inputs); + if (create && create !== 1) { + ensureSafeAssignContext(lhs); + if (lhs && !(lhs[right])) { + lhs[right] = {}; + } + } + var value = lhs != null ? lhs[right] : undefined; + if (expensiveChecks || isPossiblyDangerousMemberName(right)) { + ensureSafeObject(value, expression); + } + 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); + }; + } +}; + +/** + * @constructor + */ +var Parser = function Parser(lexer, $filter, options) { + this.lexer = lexer; + this.$filter = $filter; + this.options = options; + this.ast = new AST(lexer, options); + this.astCompiler = options.csp ? new ASTInterpreter(this.ast, $filter) : + new ASTCompiler(this.ast, $filter); +}; + +Parser.prototype = { + constructor: Parser, + + parse: function(text) { + return this.astCompiler.compile(text, this.options.expensiveChecks); + } +}; + +function isPossiblyDangerousMemberName(name) { + return name === 'constructor'; +} + +function getValueOf(value) { + return isFunction(value.valueOf) ? value.valueOf() : objectValueOf.call(value); +} + +/////////////////////////////////// + +/** + * @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'}}; + * var locals = {user:{name:'local'}}; + * + * expect(getter(context)).toEqual('angular'); + * setter(context, 'newValue'); + * expect(context.user.name).toEqual('newValue'); + * expect(getter(context, locals)).toEqual('local'); + * ``` + * + * + * @param {string} expression String expression to compile. + * @returns {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`. + * + * The returned function also has the following properties: + * * `literal` – `{boolean}` – whether the expression's top-level node is a JavaScript + * literal. + * * `constant` – `{boolean}` – whether the expression is made entirely of JavaScript + * constant literals. + * * `assign` – `{?function(context, value)}` – if the expression is assignable, this will be + * set to a function to change its value on the given context. + * + */ + + +/** + * @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 cacheDefault = createMap(); + var cacheExpensive = createMap(); + var literals = { + 'true': true, + 'false': false, + 'null': null, + 'undefined': undefined + }; + var identStart, identContinue; + + /** + * @ngdoc method + * @name $parseProvider#addLiteral + * @description + * + * Configure $parse service to add literal values that will be present as literal at expressions. + * + * @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`. + * + **/ + this.addLiteral = function(literalName, literalValue) { + literals[literalName] = literalValue; + }; + + /** + * @ngdoc method + * @name $parseProvider#setIdentifierFns + * + * @description + * + * 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. + * + * 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. + * + * @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.setIdentifierFns = function(identifierStart, identifierContinue) { + identStart = identifierStart; + identContinue = identifierContinue; + return this; + }; + + this.$get = ['$filter', function($filter) { + var noUnsafeEval = csp().noUnsafeEval; + var $parseOptions = { + csp: noUnsafeEval, + expensiveChecks: false, + literals: copy(literals), + isIdentifierStart: isFunction(identStart) && identStart, + isIdentifierContinue: isFunction(identContinue) && identContinue + }, + $parseOptionsExpensive = { + csp: noUnsafeEval, + expensiveChecks: true, + literals: copy(literals), + isIdentifierStart: isFunction(identStart) && identStart, + isIdentifierContinue: isFunction(identContinue) && identContinue + }; + var runningChecksEnabled = false; + + $parse.$$runningExpensiveChecks = function() { + return runningChecksEnabled; + }; + + return $parse; + + function $parse(exp, interceptorFn, expensiveChecks) { + var parsedExpression, oneTime, cacheKey; + + expensiveChecks = expensiveChecks || runningChecksEnabled; + + switch (typeof exp) { + case 'string': + exp = exp.trim(); + cacheKey = exp; + + var cache = (expensiveChecks ? cacheExpensive : cacheDefault); + parsedExpression = cache[cacheKey]; + + if (!parsedExpression) { + if (exp.charAt(0) === ':' && exp.charAt(1) === ':') { + oneTime = true; + exp = exp.substring(2); + } + var parseOptions = expensiveChecks ? $parseOptionsExpensive : $parseOptions; + 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; + } + if (expensiveChecks) { + parsedExpression = expensiveChecksInterceptor(parsedExpression); + } + cache[cacheKey] = parsedExpression; + } + return addInterceptor(parsedExpression, interceptorFn); + + case 'function': + return addInterceptor(exp, interceptorFn); + + default: + return addInterceptor(noop, interceptorFn); + } + } + + function expensiveChecksInterceptor(fn) { + if (!fn) return fn; + expensiveCheckFn.$$watchDelegate = fn.$$watchDelegate; + expensiveCheckFn.assign = expensiveChecksInterceptor(fn.assign); + expensiveCheckFn.constant = fn.constant; + expensiveCheckFn.literal = fn.literal; + for (var i = 0; fn.inputs && i < fn.inputs.length; ++i) { + fn.inputs[i] = expensiveChecksInterceptor(fn.inputs[i]); + } + expensiveCheckFn.inputs = fn.inputs; + + return expensiveCheckFn; + + function expensiveCheckFn(scope, locals, assign, inputs) { + var expensiveCheckOldValue = runningChecksEnabled; + runningChecksEnabled = true; + try { + return fn(scope, locals, assign, inputs); + } finally { + runningChecksEnabled = expensiveCheckOldValue; + } + } + } + + function expressionInputDirtyCheck(newValue, oldValueOfValue) { + + 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') { + // 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)) { + 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]))) { + 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) { + var unwatch, lastValue; + unwatch = scope.$watch(function oneTimeWatch(scope) { + return parsedExpression(scope); + }, /** @this */ function oneTimeListener(value, old, scope) { + lastValue = value; + if (isFunction(listener)) { + listener.apply(this, arguments); + } + if (isDefined(value)) { + scope.$$postDigest(function() { + if (isDefined(lastValue)) { + unwatch(); + } + }); + } + }, objectEquality); + return unwatch; + } + + function oneTimeLiteralWatchDelegate(scope, listener, objectEquality, parsedExpression) { + var unwatch, lastValue; + unwatch = scope.$watch(function oneTimeWatch(scope) { + return parsedExpression(scope); + }, /** @this */ function oneTimeListener(value, old, scope) { + lastValue = value; + if (isFunction(listener)) { + listener.call(this, 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 + if (parsedExpression.$$watchDelegate && + parsedExpression.$$watchDelegate !== inputsWatchDelegate) { + fn.$$watchDelegate = parsedExpression.$$watchDelegate; + } else if (!interceptorFn.$stateful) { + // If there is an interceptor, but no watchDelegate then treat the interceptor like + // we treat filters - it is assumed to be a pure function unless flagged with $stateful + fn.$$watchDelegate = inputsWatchDelegate; + useInputs = !parsedExpression.inputs; + fn.inputs = parsedExpression.inputs ? parsedExpression.inputs : [parsedExpression]; + } + + return fn; + } + }]; +} + +/** + * @ngdoc service + * @name $q + * @requires $rootScope + * @this + * + * @description + * A service that helps you run functions asynchronously, and use their return values (or exceptions) + * when they are done processing. + * + * This is an 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 + * performed asynchronously, and may or may not be finished at any given point in time. + * + * 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. + * + * ```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() { + * deferred.notify('About to greet ' + name + '.'); + * + * if (okToGreet(name)) { + * deferred.resolve('Hello, ' + name + '!'); + * } else { + * deferred.reject('Greeting ' + name + ' is not allowed.'); + * } + * }, 1000); + * + * return deferred.promise; + * } + * + * var promise = asyncGreet('Robin Hood'); + * promise.then(function(greeting) { + * alert('Success: ' + greeting); + * }, function(reason) { + * alert('Failed: ' + reason); + * }, 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 + * https://github.com/kriskowal/uncommonjs/blob/master/promises/specification.md. + * + * Additionally the promise api allows for composition that is very hard to do with the + * traditional callback ([CPS](http://en.wikipedia.org/wiki/Continuation-passing_style)) approach. + * 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()`. + * + * The purpose of the deferred object is to expose the associated Promise instance as well as APIs + * that can be used for signaling the successful or unsuccessful completion, as well as the status + * of the task. + * + * **Methods** + * + * - `resolve(value)` – resolves the derived promise with the `value`. If the value is a rejection + * 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 promise's execution. This may be called + * multiple times before the promise is either resolved or rejected. + * + * **Properties** + * + * - promise – `{Promise}` – promise object associated with this deferred. + * + * + * # The Promise API + * + * A new promise instance is created when a deferred instance is created and can be retrieved by + * calling `deferred.promise`. + * + * The purpose of the promise object is to allow for interested parties to get access to the result + * of the deferred task when it completes. + * + * **Methods** + * + * - `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` (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, 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. + * + * # 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 + * the promises at any point in the chain. This makes it possible to implement powerful APIs like + * $http's response interceptors. + * + * + * # Differences between Kris Kowal's Q and $q + * + * There are two main differences: + * + * - $q is integrated with the {@link ng.$rootScope.Scope} Scope model observation + * mechanism in angular, which means faster propagation of resolution or rejection into your + * models and avoiding unnecessary browser repaints, which would result in flickering UI. + * - 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 + * + * ```js + * it('should simulate promise', inject(function($q, $rootScope) { + * var deferred = $q.defer(); + * var promise = deferred.promise; + * var resolvedValue; + * + * promise.then(function(value) { resolvedValue = value; }); + * expect(resolvedValue).toBeUndefined(); + * + * // Simulate resolving of promise + * deferred.resolve(123); + * // Note that the 'then' function does not get called synchronously. + * // This is because we want the promise API to always be async, whether or not + * // it got called synchronously or asynchronously. + * expect(resolvedValue).toBeUndefined(); + * + * // Propagate promise resolution to 'then' functions using $apply(). + * $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. + */ +function $QProvider() { + + this.$get = ['$rootScope', '$exceptionHandler', function($rootScope, $exceptionHandler) { + return qFactory(function(callback) { + $rootScope.$evalAsync(callback); + }, $exceptionHandler); + }]; +} + +/** @this */ +function $$QProvider() { + this.$get = ['$browser', '$exceptionHandler', function($browser, $exceptionHandler) { + return qFactory(function(callback) { + $browser.defer(callback); + }, $exceptionHandler); + }]; +} + +/** + * Constructs a promise manager. + * + * @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. + * @returns {object} Promise manager. + */ +function qFactory(nextTick, exceptionHandler) { + var $qMinErr = minErr('$q', TypeError); + + /** + * @ngdoc method + * @name ng.$q#defer + * @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. + */ + function defer() { + var d = new Deferred(); + //Necessary to support unbound execution :/ + d.resolve = simpleBind(d, d.resolve); + d.reject = simpleBind(d, d.reject); + d.notify = simpleBind(d, d.notify); + return d; + } + + 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 Deferred(); + + this.$$state.pending = this.$$state.pending || []; + this.$$state.pending.push([result, onFulfilled, onRejected, progressBack]); + if (this.$$state.status > 0) scheduleProcessQueue(this.$$state); + + return result.promise; + }, + + 'catch': function(callback) { + return this.then(null, callback); + }, + + 'finally': function(callback, progressBack) { + return this.then(function(value) { + return handleCallback(value, resolve, callback); + }, function(error) { + return handleCallback(error, reject, callback); + }, progressBack); + } + }); + + //Faster, more basic than angular.bind http://jsperf.com/angular-bind-vs-custom-vs-native + function simpleBind(context, fn) { + return function(value) { + fn.call(context, value); + }; + } + + function processQueue(state) { + var fn, deferred, pending; + + pending = state.pending; + state.processScheduled = false; + state.pending = undefined; + for (var i = 0, ii = pending.length; i < ii; ++i) { + deferred = pending[i][0]; + fn = pending[i][state.status]; + try { + if (isFunction(fn)) { + deferred.resolve(fn(state.value)); + } else if (state.status === 1) { + deferred.resolve(state.value); + } else { + deferred.reject(state.value); + } + } catch (e) { + deferred.reject(e); + exceptionHandler(e); + } + } + } + + function scheduleProcessQueue(state) { + if (state.processScheduled || !state.pending) return; + state.processScheduled = true; + nextTick(function() { processQueue(state); }); + } + + function Deferred() { + this.promise = new Promise(); + } + + extend(Deferred.prototype, { + resolve: function(val) { + if (this.promise.$$state.status) return; + if (val === this.promise) { + this.$$reject($qMinErr( + 'qcycle', + 'Expected promise to be resolved with value other than itself \'{0}\'', + val)); + } else { + this.$$resolve(val); + } + + }, + + $$resolve: function(val) { + var then; + var that = this; + var done = false; + try { + if ((isObject(val) || isFunction(val))) then = val && val.then; + if (isFunction(then)) { + this.promise.$$state.status = -1; + then.call(val, resolvePromise, rejectPromise, simpleBind(this, this.notify)); + } else { + this.promise.$$state.value = val; + this.promise.$$state.status = 1; + scheduleProcessQueue(this.promise.$$state); + } + } catch (e) { + rejectPromise(e); + exceptionHandler(e); + } + + function resolvePromise(val) { + if (done) return; + done = true; + that.$$resolve(val); + } + function rejectPromise(val) { + if (done) return; + done = true; + that.$$reject(val); + } + }, + + reject: function(reason) { + if (this.promise.$$state.status) return; + this.$$reject(reason); + }, + + $$reject: function(reason) { + this.promise.$$state.value = reason; + this.promise.$$state.status = 2; + scheduleProcessQueue(this.promise.$$state); + }, + + notify: function(progress) { + var callbacks = this.promise.$$state.pending; + + if ((this.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 { + result.notify(isFunction(callback) ? callback(progress) : progress); + } catch (e) { + exceptionHandler(e); + } + } + }); + } + } + }); + + /** + * @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 + * a promise chain, you don't need to worry about it. + * + * When comparing deferreds/promises to the familiar behavior of try/catch/throw, think of + * `reject` as the `throw` keyword in JavaScript. This also means that if you "catch" an error via + * a promise error callback and you want to forward the error to the promise derived from the + * 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 + * return result; + * }, function(reason) { + * // error: handle the error if possible and + * // resolve promiseB with newPromiseOrValue, + * // otherwise forward the rejection to promiseB + * if (canHandle(reason)) { + * // handle the error and recover + * return newPromiseOrValue; + * } + * 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`. + */ + function reject(reason) { + var result = new Deferred(); + result.reject(reason); + return result.promise; + } + + 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 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 + */ + + + function when(value, callback, errback, progressBack) { + var result = new Deferred(); + result.resolve(value); + return result.promise.then(callback, errback, progressBack); + } + + /** + * @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. + * + * @param {Array.|Object.} promises An array or hash of promises. + * @returns {Promise} Returns a single promise that will be resolved with an array/hash of values, + * each value corresponding to the promise at the same index/key in the `promises` array/hash. + * 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 = new Deferred(), + counter = 0, + results = isArray(promises) ? [] : {}; + + forEach(promises, function(promise, key) { + counter++; + when(promise).then(function(value) { + results[key] = value; + if (!(--counter)) deferred.resolve(results); + }, function(reason) { + deferred.reject(reason); + }); + }); + + if (counter === 0) { + deferred.resolve(results); + } + + return deferred.promise; + } + + /** + * @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; + } + + function $Q(resolver) { + if (!isFunction(resolver)) { + throw $qMinErr('norslvr', 'Expected resolverFn, got \'{0}\'', resolver); + } + + var deferred = new Deferred(); + + function resolveFn(value) { + deferred.resolve(value); + } + + function rejectFn(reason) { + deferred.reject(reason); + } + + resolver(resolveFn, rejectFn); + + return deferred.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; +} + +/** @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; + }]; +} + +/** + * DESIGN NOTES + * + * The design decisions behind the scope are heavily favored for speed and memory consumption. + * + * The typical use of scope is to watch the expressions, which most of the time return the same + * value as last time so we optimize the operation. + * + * Closures construction is expensive in terms of speed as well as memory: + * - No closures, instead use prototypical inheritance for API + * - Internal state needs to be stored on scope directly, which means that private state is + * 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 (unshift) instead of at the end (push) + * + * Child scopes are created and removed often + * - Using an array would be slow since inserts in the middle are expensive; so we use linked lists + * + * 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 provider + * @name $rootScopeProvider + * @description + * + * Provider for the $rootScope service. + */ + +/** + * @ngdoc method + * @name $rootScopeProvider#digestTtl + * @description + * + * Sets the number of `$digest` iterations the scope should attempt to execute before giving up and + * assuming that the model is unstable. + * + * The current default is 10 iterations. + * + * In complex applications it's possible that the dependencies between `$watch`s will result in + * several digest iterations. However if an application needs more than the default 10 digest + * iterations for its model to stabilize then you should investigate what is causing the model to + * continuously change during the digest. + * + * Increasing the TTL could have performance implications, so you should not change it without + * proper justification. + * + * @param {number} limit The number of digest iterations. + */ + + +/** + * @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 event emission/broadcast and subscription facility. See the + * {@link guide/scope developer guide on scopes}. + */ +function $RootScopeProvider() { + var TTL = 10; + var $rootScopeMinErr = minErr('$rootScope'); + var lastDirtyWatch = null; + var applyAsyncId = null; + + this.digestTtl = function(value) { + if (arguments.length) { + TTL = value; + } + return TTL; + }; + + 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) { + + 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 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#$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. + * + * + * # Inheritance + * A scope can inherit from a parent scope, as in this example: + * ```js + var parent = $rootScope; + var child = parent.$new(); + + parent.salutation = "Hello"; + 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 + * provided for the current scope. Defaults to {@link ng}. + * @param {Object.=} instanceCache Provides pre-instantiated services which should + * append/override services provided by `providers`. This is handy + * when unit-testing and having the need to override a default + * service. + * @returns {Object} Newly created scope. + * + */ + function Scope() { + this.$id = nextUid(); + this.$$phase = this.$parent = this.$$watchers = + this.$$nextSibling = this.$$prevSibling = + this.$$childHead = this.$$childTail = null; + this.$root = this; + this.$$destroyed = false; + this.$$listeners = {}; + this.$$listenerCount = {}; + this.$$watchersCount = 0; + this.$$isolateBindings = null; + } + + /** + * @ngdoc property + * @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 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#$digest $digest()} event. + * The scope can be removed from the scope hierarchy using {@link ng.$rootScope.Scope#$destroy $destroy()}. + * + * {@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. + * + * @param {boolean} isolate If true, then the scope does not prototypically inherit from the + * parent scope. The scope is isolated, as it can not see parent scope properties. + * 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, parent) { + var child; + + parent = parent || this; + + if (isolate) { + child = new Scope(); + child.$root = this.$root; + } else { + // 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.$parent = parent; + child.$$prevSibling = parent.$$childTail; + if (parent.$$childHead) { + parent.$$childTail.$$nextSibling = child; + parent.$$childTail = child; + } else { + 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 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#$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). 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. + * - 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#$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#$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. + * + * + * + * # Example + * ```js + // let's assume that scope was dependency injected as the $rootScope + var scope = $rootScope; + scope.name = 'misko'; + scope.counter = 0; + + expect(scope.counter).toEqual(0); + scope.$watch('name', function(newValue, oldValue) { + scope.counter = scope.counter + 1; + }); + expect(scope.counter).toEqual(0); + + scope.$digest(); + // 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(2); + + + + // Using a function as a watchExpression + var food; + scope.foodCounter = 0; + expect(scope.foodCounter).toEqual(0); + scope.$watch( + // This function returns the value being watched. It is called for each turn of the $digest loop + function() { return food; }, + // 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 + scope.foodCounter = scope.foodCounter + 1; + } + } + ); + // No digest has been run so the counter will be zero + expect(scope.foodCounter).toEqual(0); + + // Run the digest but since food has not changed count will still be zero + scope.$digest(); + expect(scope.foodCounter).toEqual(0); + + // Update food and run digest. Now the counter will increment + food = 'cheeseburger'; + scope.$digest(); + expect(scope.foodCounter).toEqual(1); + + * ``` + * + * + * + * @param {(function()|string)} watchExpression Expression that is evaluated on each + * {@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(newVal, oldVal, scope)} listener Callback called whenever the value + * of `watchExpression` changes. + * + * - `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, prettyPrintExpression) { + var get = $parse(watchExp); + + if (get.$$watchDelegate) { + return get.$$watchDelegate(this, listener, objectEquality, get, watchExp); + } + var scope = this, + array = scope.$$watchers, + watcher = { + fn: listener, + last: initWatchVal, + get: get, + exp: prettyPrintExpression || watchExp, + eq: !!objectEquality + }; + + lastDirtyWatch = null; + + if (!isFunction(listener)) { + 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 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. + * + * @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` + * 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 method + * @name $rootScope.Scope#$watchCollection + * @kind function + * + * @description + * Shallow watches the properties of an object and fires whenever any of the properties change + * (for arrays, this implies watching the array items; for object maps, this implies watching + * the properties). If a change is detected, the `listener` callback is fired. + * + * - The `obj` collection is observed via standard $watch operation and is examined on every + * call to $digest() to see if any items have been added, removed, or moved. + * - The `listener` is called whenever anything within the `obj` has changed. Examples include + * adding, removing, and moving items belonging to an object or array. + * + * + * # Example + * ```js + $scope.names = ['igor', 'matias', 'misko', 'james']; + $scope.dataCount = 4; + + $scope.$watchCollection('names', function(newNames, oldNames) { + $scope.dataCount = newNames.length; + }); + + expect($scope.dataCount).toEqual(4); + $scope.$digest(); + + //still at 4 ... no changes + expect($scope.dataCount).toEqual(4); + + $scope.names.pop(); + $scope.$digest(); + + //now there's been a change + expect($scope.dataCount).toEqual(3); + * ``` + * + * + * @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#$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 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; + // 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 changeDetector = $parse(obj, $watchCollectionInterceptor); + var internalArray = []; + var internalObject = {}; + var initRun = true; + var oldLength = 0; + + function $watchCollectionInterceptor(_value) { + newValue = _value; + var newLength, key, bothNaN, newItem, oldItem; + + // 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++; + } + } else if (isArrayLike(newValue)) { + if (oldValue !== internalArray) { + // we are transitioning from something which was not an array into array. + oldValue = internalArray; + oldLength = oldValue.length = 0; + changeDetected++; + } + + newLength = newValue.length; + + if (oldLength !== newLength) { + // if lengths do not match we need to trigger change notification + changeDetected++; + oldValue.length = oldLength = newLength; + } + // copy the items to oldValue and look for changes. + for (var i = 0; i < newLength; 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] = newItem; + } + } + } else { + if (oldValue !== internalObject) { + // we are transitioning from something which was not an object into object. + oldValue = internalObject = {}; + oldLength = 0; + changeDetected++; + } + // copy the items to oldValue and look for changes. + newLength = 0; + for (key in newValue) { + if (hasOwnProperty.call(newValue, key)) { + newLength++; + 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] = newItem; + } + } else { + oldLength++; + oldValue[key] = newItem; + changeDetected++; + } + } + } + if (oldLength > newLength) { + // we used to have more keys, need to find them and destroy them. + changeDetected++; + for (key in oldValue) { + if (!hasOwnProperty.call(newValue, key)) { + oldLength--; + delete oldValue[key]; + } + } + } + } + return changeDetected; + } + + function $watchCollectionAction() { + 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(changeDetector, $watchCollectionAction); + }, + + /** + * @ngdoc method + * @name $rootScope.Scope#$digest + * @kind function + * + * @description + * 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#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#$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; + + expect(scope.counter).toEqual(0); + scope.$watch('name', function(newValue, oldValue) { + scope.counter = scope.counter + 1; + }); + expect(scope.counter).toEqual(0); + + scope.$digest(); + // 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(2); + * ``` + * + */ + $digest: function() { + 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; + + do { // "while dirty" loop + dirty = false; + current = target; + + // 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 $applyAsync would + // lead to a '$digest already in progress' error. + for (var asyncQueuePosition = 0; asyncQueuePosition < asyncQueue.length; asyncQueuePosition++) { + try { + asyncTask = asyncQueue[asyncQueuePosition]; + asyncTask.scope.$eval(asyncTask.expression, asyncTask.locals); + } catch (e) { + $exceptionHandler(e); + } + lastDirtyWatch = null; + } + asyncQueue.length = 0; + + 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) { + $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.$$watchersCount && current.$$childHead) || + (current !== target && current.$$nextSibling)))) { + while (current !== target && !(next = current.$$nextSibling)) { + current = current.$parent; + } + } + } while ((current = next)); + + // `break traverseScopesLoop;` takes us to here + + if ((dirty || asyncQueue.length) && !(ttl--)) { + clearPhase(); + throw $rootScopeMinErr('infdig', + '{0} $digest() iterations reached. Aborting!\n' + + 'Watchers fired in the last 5 iterations: {1}', + TTL, watchLog); + } + + } while (dirty || asyncQueue.length); + + clearPhase(); + + // postDigestQueuePosition isn't local here because this loop can be reentered recursively. + while (postDigestQueuePosition < postDigestQueue.length) { + try { + postDigestQueue[postDigestQueuePosition++](); + } catch (e) { + $exceptionHandler(e); + } + } + postDigestQueue.length = postDigestQueuePosition = 0; + }, + + + /** + * @ngdoc event + * @name $rootScope.Scope#$destroy + * @eventType broadcast on scope being destroyed + * + * @description + * Broadcasted when a scope and its children are being destroyed. + * + * Note that, in AngularJS, there is also a `$destroy` jQuery event, which can be used to + * clean up DOM bindings before an element is removed from the DOM. + */ + + /** + * @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#$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. + * + * The `$destroy()` is usually used by directives such as + * {@link ng.directive:ngRepeat ngRepeat} for managing the + * unrolling of the loop. + * + * Just before a scope is destroyed, a `$destroy` event is broadcasted on this scope. + * Application code can register a `$destroy` event handler that will give it a chance to + * perform any necessary cleanup. + * + * Note that, in AngularJS, there is also a `$destroy` jQuery event, which can be used to + * clean up DOM bindings before an element is removed from the DOM. + */ + $destroy: function() { + // 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) { + //Remove handlers attached to window when $rootScope is removed + $browser.$$applicationDestroyed(); + } + + 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; + + // 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 method + * @name $rootScope.Scope#$eval + * @kind function + * + * @description + * Executes the `expression` on the current scope and returns the result. Any exceptions in + * the expression are propagated (uncaught). This is useful when evaluating Angular + * 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. + * + * - `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. + * @returns {*} The result of evaluating the expression. + */ + $eval: function(expr, locals) { + return $parse(expr)(this, locals); + }, + + /** + * @ngdoc method + * @name $rootScope.Scope#$evalAsync + * @kind function + * + * @description + * Executes the expression on the current scope at a later point in time. + * + * The `$evalAsync` makes no guarantees as to when the `expression` will be executed, only + * that: + * + * - it will execute after the function that scheduled the evaluation (preferably before DOM + * rendering). + * - 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 + * {@link ng.$exceptionHandler $exceptionHandler} service. + * + * __Note:__ if this function is called outside of a `$digest` cycle, a new `$digest` cycle + * will be scheduled. However, it is encouraged to always call code that changes the model + * from within an `$apply` call. That includes code evaluated via `$evalAsync`. + * + * @param {(string|function())=} expression An angular expression to be executed. + * + * - `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, 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 && !asyncQueue.length) { + $browser.defer(function() { + if (asyncQueue.length) { + $rootScope.$digest(); + } + }); + } + + asyncQueue.push({scope: this, expression: $parse(expr), locals: locals}); + }, + + $$postDigest: function(fn) { + postDigestQueue.push(fn); + }, + + /** + * @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#$digest executing watches}. + * + * ## Life cycle + * + * # Pseudo-Code of `$apply()` + * ```js + function $apply(expr) { + try { + return $eval(expr); + } catch (e) { + $exceptionHandler(e); + } finally { + $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#$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#$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. + * + * - `string`: execute using the rules as defined in {@link guide/expression expression}. + * - `function(scope)`: execute the function with current `scope` parameter. + * + * @returns {*} The result of evaluating the expression. + */ + $apply: function(expr) { + try { + beginPhase('$apply'); + try { + return this.$eval(expr); + } finally { + clearPhase(); + } + } catch (e) { + $exceptionHandler(e); + } finally { + try { + $rootScope.$digest(); + } catch (e) { + $exceptionHandler(e); + // eslint-disable-next-line no-unsafe-finally + throw e; + } + } + }, + + /** + * @ngdoc method + * @name $rootScope.Scope#$applyAsync + * @kind function + * + * @description + * 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 + * passed into the listener has the following attributes: + * + * - `targetScope` - `{Scope}`: the scope on which the event was `$emit`-ed or + * `$broadcast`-ed. + * - `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). + * - `preventDefault` - `{function}`: calling `preventDefault` sets `defaultPrevented` flag + * to true. + * - `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. + * @returns {function()} Returns a deregistration function for this listener. + */ + $on: function(name, listener) { + var namedListeners = this.$$listeners[name]; + if (!namedListeners) { + this.$$listeners[name] = namedListeners = []; + } + namedListeners.push(listener); + + var current = this; + do { + if (!current.$$listenerCount[name]) { + current.$$listenerCount[name] = 0; + } + current.$$listenerCount[name]++; + } while ((current = current.$parent)); + + var self = this; + return function() { + var indexOfListener = namedListeners.indexOf(listener); + if (indexOfListener !== -1) { + namedListeners[indexOfListener] = null; + decrementListenerCount(self, 1, name); + } + }; + }, + + + /** + * @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#$on} listeners. + * + * The event life cycle starts at the scope on which `$emit` was called. All + * {@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#$on listeners} will be passed + * onto the {@link ng.$exceptionHandler $exceptionHandler} service. + * + * @param {string} name Event name to emit. + * @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 + }, + listenerArgs = concat([event], arguments, 1), + i, length; + + do { + namedListeners = scope.$$listeners[name] || empty; + event.currentScope = scope; + for (i = 0, length = namedListeners.length; i < length; i++) { + + // if listeners were deregistered, defragment the array + if (!namedListeners[i]) { + namedListeners.splice(i, 1); + i--; + length--; + continue; + } + try { + //allow all listeners attached to the current scope to run + namedListeners[i].apply(null, listenerArgs); + } catch (e) { + $exceptionHandler(e); + } + } + //if any listener on the current scope stops propagation, prevent bubbling + if (stopPropagation) { + event.currentScope = null; + return event; + } + //traverse upwards + scope = scope.$parent; + } while (scope); + + event.currentScope = null; + + return event; + }, + + + /** + * @ngdoc method + * @name $rootScope.Scope#$broadcast + * @kind function + * + * @description + * Dispatches an event `name` downwards to all child scopes (and their children) notifying the + * registered {@link ng.$rootScope.Scope#$on} listeners. + * + * The event life cycle starts at the scope on which `$broadcast` was called. All + * {@link ng.$rootScope.Scope#$on listeners} listening for `name` event on this scope get + * notified. Afterwards, the event propagates to all direct and indirect scopes of the current + * scope and calls all registered listeners along the way. The event cannot be canceled. + * + * 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 broadcast. + * @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} + */ + $broadcast: function(name, args) { + var target = this, + current = target, + next = target, + event = { + name: name, + targetScope: target, + preventDefault: function() { + event.defaultPrevented = true; + }, + defaultPrevented: false + }; + + if (!target.$$listenerCount[name]) return event; + + var listenerArgs = concat([event], arguments, 1), + listeners, i, length; + + //down while you can, then up and next sibling or up and next sibling until back at root + while ((current = next)) { + event.currentScope = current; + listeners = current.$$listeners[name] || []; + for (i = 0, length = listeners.length; i < length; i++) { + // if listeners were deregistered, defragment the array + if (!listeners[i]) { + listeners.splice(i, 1); + i--; + length--; + continue; + } + + try { + listeners[i].apply(null, listenerArgs); + } 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 $digest + // (though it differs due to having the extra check for $$listenerCount) + if (!(next = ((current.$$listenerCount[name] && current.$$childHead) || + (current !== target && current.$$nextSibling)))) { + while (current !== target && !(next = current.$$nextSibling)) { + current = current.$parent; + } + } + } + + event.currentScope = null; + return event; + } + }; + + var $rootScope = new Scope(); + + //The internal queues. Expose them on the $rootScope for debugging/testing purposes. + var asyncQueue = $rootScope.$$asyncQueue = []; + var postDigestQueue = $rootScope.$$postDigestQueue = []; + var applyAsyncQueue = $rootScope.$$applyAsyncQueue = []; + + var postDigestQueuePosition = 0; + + return $rootScope; + + + function beginPhase(phase) { + if ($rootScope.$$phase) { + throw $rootScopeMinErr('inprog', '{0} already in progress', $rootScope.$$phase); + } + + $rootScope.$$phase = phase; + } + + function clearPhase() { + $rootScope.$$phase = null; + } + + function incrementWatchersCount(current, count) { + do { + current.$$watchersCount += count; + } while ((current = current.$parent)); + } + + function decrementListenerCount(current, count, name) { + do { + current.$$listenerCount[name] -= count; + + if (current.$$listenerCount[name] === 0) { + delete current.$$listenerCount[name]; + } + } while ((current = current.$parent)); + } + + /** + * function used as an initial value for watchers. + * because it's unique we can easily tell it apart from other values + */ + function initWatchVal() {} + + function flushApplyAsync() { + while (applyAsyncQueue.length) { + try { + applyAsyncQueue.shift()(); + } catch (e) { + $exceptionHandler(e); + } + } + applyAsyncId = null; + } + + function scheduleApplyAsync() { + if (applyAsyncId === null) { + applyAsyncId = $browser.defer(function() { + $rootScope.$apply(flushApplyAsync); + }); + } + } + }]; +} + +/** + * @ngdoc service + * @name $rootElement + * + * @description + * The root element of Angular application. This is either the element where {@link + * ng.directive:ngApp ngApp} was declared or the element passed into + * {@link angular.bootstrap}. The element represents the root element of application. It is also the + * location where the application's {@link auto.$injector $injector} service gets + * published, and can be retrieved using `$rootElement.injector()`. + */ + + +// the implementation is in angular.bootstrap + +/** + * @this + * @description + * Private service to sanitize uris for links and images. Used by $compile and $sanitize. + */ +function $$SanitizeUriProvider() { + var aHrefSanitizationWhitelist = /^\s*(https?|ftp|mailto|tel|file):/, + imgSrcSanitizationWhitelist = /^\s*((https?|ftp|file|blob):|data:image\/)/; + + /** + * @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. + * + * 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` + * regular expression. If a match is found, the original url is written into the dom. Otherwise, + * the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM. + * + * @param {RegExp=} regexp New regexp to whitelist urls with. + * @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for + * chaining otherwise. + */ + this.aHrefSanitizationWhitelist = function(regexp) { + if (isDefined(regexp)) { + aHrefSanitizationWhitelist = regexp; + return this; + } + return aHrefSanitizationWhitelist; + }; + + + /** + * @description + * Retrieves or overrides the default regular expression that is used for whitelisting of safe + * urls during img[src] sanitization. + * + * The sanitization is a security measure aimed at prevent XSS attacks via html links. + * + * Any url about to be assigned to img[src] via data-binding is first normalized and turned into + * an absolute url. Afterwards, the url is matched against the `imgSrcSanitizationWhitelist` + * regular expression. If a match is found, the original url is written into the dom. Otherwise, + * the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM. + * + * @param {RegExp=} regexp New regexp to whitelist urls with. + * @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for + * chaining otherwise. + */ + this.imgSrcSanitizationWhitelist = function(regexp) { + if (isDefined(regexp)) { + imgSrcSanitizationWhitelist = regexp; + return this; + } + return imgSrcSanitizationWhitelist; + }; + + this.$get = function() { + return function sanitizeUri(uri, isImage) { + var regex = isImage ? imgSrcSanitizationWhitelist : aHrefSanitizationWhitelist; + var 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: 'html', + CSS: 'css', + 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: 'resourceUrl', + JS: 'js' +}; + +// Helper functions follow. + +function adjustMatcher(matcher) { + if (matcher === 'self') { + return matcher; + } else if (isString(matcher)) { + // Strings match exactly except for 2 wildcards - '*' and '**'. + // '*' matches any character except those from the set ':/.?&'. + // '**' matches any character (like .* in a RegExp). + // More than 2 *'s raises an error as it's ill defined. + if (matcher.indexOf('***') > -1) { + throw $sceMinErr('iwcard', + 'Illegal sequence *** in string matcher. String: {0}', matcher); + } + matcher = escapeForRegexp(matcher). + replace(/\\\*\\\*/g, '.*'). + replace(/\\\*/g, '[^:/.?&;]*'); + return new RegExp('^' + matcher + '$'); + } else if (isRegExp(matcher)) { + // The only other type of matcher allowed is a Regexp. + // Match entire URL / disallow partial matches. + // Flags are reset (i.e. no global, ignoreCase or multiline) + return new RegExp('^' + matcher.source + '$'); + } else { + throw $sceMinErr('imatcher', + 'Matchers may only be "self", string patterns or RegExp objects'); + } +} + + +function adjustMatchers(matchers) { + var adjustedMatchers = []; + if (isDefined(matchers)) { + forEach(matchers, function(matcher) { + adjustedMatchers.push(adjustMatcher(matcher)); + }); + } + return adjustedMatchers; +} + + +/** + * @ngdoc service + * @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. + * + * 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 + * because, while the `$sce` provides numerous shorthand methods, etc., you really only need to + * override 3 core functions (`trustAs`, `getTrusted` and `valueOf`) to replace the way things + * work because `$sce` delegates to `$sceDelegate` for these operations. + * + * Refer {@link ng.$sceDelegateProvider $sceDelegateProvider} to configure this service. + * + * The default instance of `$sceDelegate` should work out of the box with little pain. While you + * 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#resourceUrlWhitelist + * $sceDelegateProvider.resourceUrlWhitelist} and {@link + * ng.$sceDelegateProvider#resourceUrlBlacklist $sceDelegateProvider.resourceUrlBlacklist} + */ + +/** + * @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#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)}. + * + * **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. + * - 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/**' + * ]); + * + * // The blacklist overrides the whitelist so the open redirect here is blocked. + * $sceDelegateProvider.resourceUrlBlacklist([ + * 'http://myapp.example.com/clickThru**' + * ]); + * }); + * ``` + */ + +function $SceDelegateProvider() { + this.SCE_CONTEXTS = SCE_CONTEXTS; + + // Resource URLs can also be trusted by policy. + var resourceUrlWhitelist = ['self'], + resourceUrlBlacklist = []; + + /** + * @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. + * + * 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. + */ + this.resourceUrlWhitelist = function(value) { + if (arguments.length) { + resourceUrlWhitelist = adjustMatchers(value); + } + return resourceUrlWhitelist; + }; + + /** + * @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. + * + * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items + * 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.) + * + * @description + * Sets/Gets the blacklist of trusted resource URLs. + */ + + this.resourceUrlBlacklist = function(value) { + if (arguments.length) { + resourceUrlBlacklist = adjustMatchers(value); + } + return resourceUrlBlacklist; + }; + + this.$get = ['$injector', function($injector) { + + var htmlSanitizer = function htmlSanitizer(html) { + throw $sceMinErr('unsafe', 'Attempting to use an unsafe value in a safe context.'); + }; + + if ($injector.has('$sanitize')) { + htmlSanitizer = $injector.get('$sanitize'); + } + + + function matchUrl(matcher, parsedUrl) { + if (matcher === 'self') { + return urlIsSameOrigin(parsedUrl); + } else { + // definitely a regex. See adjustMatchers() + return !!matcher.exec(parsedUrl.href); + } + } + + function isResourceUrlAllowedByPolicy(url) { + var parsedUrl = urlResolve(url.toString()); + var i, n, allowed = false; + // Ensure that at least one item from the whitelist allows this url. + for (i = 0, n = resourceUrlWhitelist.length; i < n; i++) { + if (matchUrl(resourceUrlWhitelist[i], parsedUrl)) { + allowed = true; + break; + } + } + if (allowed) { + // Ensure that no item from the blacklist blocked this url. + for (i = 0, n = resourceUrlBlacklist.length; i < n; i++) { + if (matchUrl(resourceUrlBlacklist[i], parsedUrl)) { + allowed = false; + break; + } + } + } + return allowed; + } + + function generateHolderType(Base) { + var holderType = function TrustedValueHolderType(trustedValue) { + this.$$unwrapTrustedValue = function() { + return trustedValue; + }; + }; + if (Base) { + holderType.prototype = new Base(); + } + holderType.prototype.valueOf = function sceValueOf() { + return this.$$unwrapTrustedValue(); + }; + holderType.prototype.toString = function sceToString() { + return this.$$unwrapTrustedValue().toString(); + }; + return holderType; + } + + var trustedValueHolderBase = generateHolderType(), + byType = {}; + + byType[SCE_CONTEXTS.HTML] = generateHolderType(trustedValueHolderBase); + byType[SCE_CONTEXTS.CSS] = generateHolderType(trustedValueHolderBase); + byType[SCE_CONTEXTS.URL] = generateHolderType(trustedValueHolderBase); + byType[SCE_CONTEXTS.JS] = generateHolderType(trustedValueHolderBase); + byType[SCE_CONTEXTS.RESOURCE_URL] = generateHolderType(byType[SCE_CONTEXTS.URL]); + + /** + * @ngdoc method + * @name $sceDelegate#trustAs + * + * @description + * Returns an object that is trusted by angular for use in specified strict + * contextual escaping contexts (such as ng-bind-html, 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. + * + * @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. + */ + 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); + } + 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); + } + return new Constructor(trustedValue); + } + + /** + * @ngdoc method + * @name $sceDelegate#valueOf + * + * @description + * 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#trustAs `$sceDelegate.trustAs`}, returns it as-is. + * + * @param {*} value The result of a prior {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs`} + * call or anything else. + * @returns {*} 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) { + if (maybeTrusted instanceof trustedValueHolderBase) { + return maybeTrusted.$$unwrapTrustedValue(); + } else { + return maybeTrusted; + } + } + + /** + * @ngdoc method + * @name $sceDelegate#getTrusted + * + * @description + * Takes the result of a {@link ng.$sceDelegate#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. + * + *
      + * Disabling auto-escaping is extremely dangerous, it usually creates a Cross Site Scripting + * (XSS) vulnerability in your application. + *
      + * + * @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#trustAs + * `$sceDelegate.trustAs`} call. + * @returns {*} The value the was originally provided to {@link ng.$sceDelegate#trustAs + * `$sceDelegate.trustAs`} if valid in this context. Otherwise, throws an exception. + */ + function getTrusted(type, maybeTrusted) { + if (maybeTrusted === null || isUndefined(maybeTrusted) || maybeTrusted === '') { + return maybeTrusted; + } + var constructor = (byType.hasOwnProperty(type) ? byType[type] : null); + 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. + if (type === SCE_CONTEXTS.RESOURCE_URL) { + if (isResourceUrlAllowedByPolicy(maybeTrusted)) { + return maybeTrusted; + } else { + throw $sceMinErr('insecurl', + 'Blocked loading resource from url not allowed by $sceDelegate policy. URL: {0}', + maybeTrusted.toString()); + } + } else if (type === SCE_CONTEXTS.HTML) { + return htmlSanitizer(maybeTrusted); + } + throw $sceMinErr('unsafe', 'Attempting to use an unsafe value in a safe context.'); + } + + return { trustAs: trustAs, + getTrusted: getTrusted, + valueOf: valueOf }; + }]; +} + + +/** + * @ngdoc provider + * @name $sceProvider + * @this + * + * @description + * + * The $sceProvider provider allows developers to configure the {@link ng.$sce $sce} service. + * - enable/disable Strict Contextual Escaping (SCE) in a module + * - override the default implementation with a custom delegate + * + * Read more about {@link ng.$sce Strict Contextual Escaping (SCE)}. + */ + +/** + * @ngdoc service + * @name $sce + * @kind function + * + * @description + * + * `$sce` is a service that provides Strict Contextual Escaping services to AngularJS. + * + * # 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. + * + * As of version 1.2, Angular ships with SCE enabled by default. + * + * Note: When enabled (the default), IE<11 in quirks mode is not supported. In this mode, IE<11 allow + * 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. + * + * SCE assists 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. + * + * 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.) + * + * 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. + * + * How would you ensure that every place that used these types of bindings was bound to a value that + * was sanitized by your library (or returned as safe for rendering by your server?) How can you + * 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#trustAs $sce.trustAs} + * (and shorthand methods such as {@link ng.$sce#trustAsHtml $sce.trustAsHtml}, etc.) to + * obtain values that will be accepted by SCE / privileged contexts. + * + * + * ## How does it work? + * + * 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. 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. + * + * As an example, {@link ng.directive:ngBindHtml ngBindHtml} uses {@link + * 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 || ''); + * }); + * }; + * }]; + * ``` + * + * ## Impact on loading templates + * + * This applies both to the {@link ng.directive:ngInclude `ng-include`} directive as well as + * `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#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 + * [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 + * + * 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 (remember to include the `ngSanitize` module) (e.g. + * `
      `) just works. + * + * Additionally, `a[href]` and `img[src]` automatically sanitize their URLs and do not pass them + * through {@link ng.$sce#getTrusted $sce.getTrusted}. SCE doesn't play a role here. + * + * 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#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 + * security onto an application later. + * + * + * ## What trusted context types are supported? + * + * | 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. If an unsafe value is encountered and the {@link ngSanitize $sanitize} module is present this will sanitize the value instead of throwing an error. | + * | `$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. | + * + * ## 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: + * + * - **'self'** + * - The special **string**, `'self'`, can be used to match against all URLs of the **same + * domain** as the application document using the **same protocol**. + * - **String** (except the special value `'self'`) + * - The string is matched against the full *normalized / absolute URL* of the resource + * being tested (substring matches are not good enough.) + * - There are exactly **two wildcard sequences** - `*` and `**`. All other characters + * match themselves. + * - `*`: 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 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.) 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 + * 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 + * 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 + * 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 + * [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 + * Closure library's [goog.string.regExpEscape(s)]( + * http://docs.closure-library.googlecode.com/git/closure_goog_string_string.js.source.html#line962). + * + * Refer {@link ng.$sceDelegateProvider $sceDelegateProvider} for an example. + * + * ## Show me an example using SCE. + * + * + * + *
      + *

      + * 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}).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.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.'); + * }); + * }); + * + *
      + * + * + * + * ## Can I disable SCE completely? + * + * Yes, you can. However, this is strongly discouraged. SCE gives you a lot of security benefits + * 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. + * + * 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); + * }); + * ``` + * + */ + +function $SceProvider() { + var enabled = true; + + /** + * @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. + * + * @description + * Enables/disables SCE and returns the current value. + */ + this.enabled = function(value) { + if (arguments.length) { + enabled = !!value; + } + return enabled; + }; + + + /* 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. + */ + + this.$get = ['$parse', '$sceDelegate', function( + $parse, $sceDelegate) { + // 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 && msie < 8) { + throw $sceMinErr('iequirks', + '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 = shallowCopy(SCE_CONTEXTS); + + /** + * @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}. + * + * @description + * Returns a boolean indicating if SCE is enabled. + */ + sce.isEnabled = function() { + return enabled; + }; + sce.trustAs = $sceDelegate.trustAs; + sce.getTrusted = $sceDelegate.getTrusted; + sce.valueOf = $sceDelegate.valueOf; + + if (!enabled) { + sce.trustAs = sce.getTrusted = function(type, value) { return value; }; + sce.valueOf = identity; + } + + /** + * @ngdoc method + * @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#getTrusted $sce.getTrusted(*type*, + * *result*)} + * + * @param {string} type The kind of 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: + * + * * `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 $parse(expr, function(value) { + return sce.getTrusted(type, value); + }); + } + }; + + /** + * @ngdoc method + * @name $sce#trustAs + * + * @description + * Delegates to {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}. As such, + * returns an object that is trusted by angular for use in specified strict contextual + * escaping contexts (such as ng-bind-html, 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. + * + * @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. + */ + + /** + * @ngdoc method + * @name $sce#trustAsHtml + * + * @description + * Shorthand method. `$sce.trustAsHtml(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#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#trustAs $sce.trustAs}.) + */ + + /** + * @ngdoc method + * @name $sce#trustAsUrl + * + * @description + * Shorthand method. `$sce.trustAsUrl(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#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#trustAs $sce.trustAs}.) + */ + + /** + * @ngdoc method + * @name $sce#trustAsResourceUrl + * + * @description + * Shorthand method. `$sce.trustAsResourceUrl(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#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#trustAs $sce.trustAs}.) + */ + + /** + * @ngdoc method + * @name $sce#trustAsJs + * + * @description + * Shorthand method. `$sce.trustAsJs(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#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#trustAs $sce.trustAs}.) + */ + + /** + * @ngdoc method + * @name $sce#getTrusted + * + * @description + * Delegates to {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted`}. As such, + * takes the result of a {@link ng.$sce#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. + * + * @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#trustAs `$sce.trustAs`} + * call. + * @returns {*} The value the was originally provided to + * {@link ng.$sce#trustAs `$sce.trustAs`} if valid in this context. + * Otherwise, throws an exception. + */ + + /** + * @ngdoc method + * @name $sce#getTrustedHtml + * + * @description + * Shorthand method. `$sce.getTrustedHtml(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)` + */ + + /** + * @ngdoc method + * @name $sce#getTrustedCss + * + * @description + * Shorthand method. `$sce.getTrustedCss(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)` + */ + + /** + * @ngdoc method + * @name $sce#getTrustedUrl + * + * @description + * Shorthand method. `$sce.getTrustedUrl(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)` + */ + + /** + * @ngdoc method + * @name $sce#getTrustedResourceUrl + * + * @description + * Shorthand method. `$sce.getTrustedResourceUrl(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)` + */ + + /** + * @ngdoc method + * @name $sce#getTrustedJs + * + * @description + * Shorthand method. `$sce.getTrustedJs(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)` + */ + + /** + * @ngdoc method + * @name $sce#parseAsHtml + * + * @description + * Shorthand method. `$sce.parseAsHtml(expression string)` → + * {@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: + * + * * `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 $sce#parseAsCss + * + * @description + * Shorthand method. `$sce.parseAsCss(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: + * + * * `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 $sce#parseAsUrl + * + * @description + * Shorthand method. `$sce.parseAsUrl(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: + * + * * `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 $sce#parseAsResourceUrl + * + * @description + * Shorthand method. `$sce.parseAsResourceUrl(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: + * + * * `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 $sce#parseAsJs + * + * @description + * Shorthand method. `$sce.parseAsJs(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: + * + * * `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. + var parse = sce.parseAs, + getTrusted = sce.getTrusted, + trustAs = sce.trustAs; + + forEach(SCE_CONTEXTS, function(enumValue, name) { + var lName = lowercase(name); + sce[camelCase('parse_as_' + lName)] = function(expr) { + return parse(enumValue, expr); + }; + sce[camelCase('get_trusted_' + lName)] = function(value) { + return getTrusted(enumValue, value); + }; + sce[camelCase('trust_as_' + lName)] = function(value) { + return trustAs(enumValue, value); + }; + }); + + return sce; + }]; +} + +/* exported $SnifferProvider */ + +/** + * !!! This is an undocumented "private" service !!! + * + * @name $sniffer + * @requires $window + * @requires $document + * @this + * + * @property {boolean} history Does the browser support html5 history api ? + * @property {boolean} transitions Does the browser support CSS transition events ? + * @property {boolean} animations Does the browser support CSS animation events ? + * + * @description + * This is very simple implementation of testing browser's features. + */ +function $SnifferProvider() { + this.$get = ['$window', '$document', function($window, $document) { + var eventSupport = {}, + // 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). + isChromePackagedApp = + $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] || {}, + vendorPrefix, + vendorRegex = /^(Moz|webkit|ms)(?=[A-Z])/, + bodyStyle = document.body && document.body.style, + transitions = false, + animations = false, + match; + + if (bodyStyle) { + for (var prop in bodyStyle) { + if ((match = vendorRegex.exec(prop))) { + vendorPrefix = match[0]; + vendorPrefix = vendorPrefix[0].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(bodyStyle.webkitTransition); + animations = isString(bodyStyle.webkitAnimation); + } + } + + + return { + // Android has history.pushState, but it does not update location correctly + // so let's not use the history API at all. + // http://code.google.com/p/android/issues/detail?id=17471 + // https://github.com/angular/angular.js/issues/904 + + // 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 + history: !!(hasHistoryPushState && !(android < 4) && !boxee), + hasEvent: function(event) { + // 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. + // 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 <= 11) return false; + + if (isUndefined(eventSupport[event])) { + var divElm = document.createElement('div'); + eventSupport[event] = 'on' + event in divElm; + } + + return eventSupport[event]; + }, + csp: csp(), + vendorPrefix: vendorPrefix, + 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 = ['$templateCache', '$http', '$q', '$sce', function($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) { + throw $templateRequestMinErr('tpload', 'Failed to load template: {0} (HTTP status: {1} {2})', + tpl, resp.status, resp.statusText); + } + 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', '$$q', '$exceptionHandler', + function($rootScope, $browser, $q, $$q, $exceptionHandler) { + + var deferreds = {}; + + + /** + * @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 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. + * + * 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#$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) { + 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.apply(null, args)); + } catch (e) { + deferred.reject(e); + $exceptionHandler(e); + } finally { + delete deferreds[promise.$$timeoutId]; + } + + if (!skipApply) $rootScope.$apply(); + }, delay); + + promise.$$timeoutId = timeoutId; + deferreds[timeoutId] = deferred; + + return promise; + } + + + /** + * @ngdoc method + * @name $timeout#cancel + * + * @description + * Cancels a task associated with the `promise`. As a result of this, the promise will be + * resolved with a rejection. + * + * @param {Promise=} promise Promise returned by the `$timeout` function. + * @returns {boolean} Returns `true` if the task hasn't executed yet and was successfully + * canceled. + */ + timeout.cancel = function(promise) { + if (promise && promise.$$timeoutId in deferreds) { + deferreds[promise.$$timeoutId].reject('canceled'); + delete deferreds[promise.$$timeoutId]; + return $browser.defer.cancel(promise.$$timeoutId); + } + return false; + }; + + return timeout; + }]; +} + +// NOTE: The usage of window and document instead of $window and $document here is +// deliberate. This service depends on the specific behavior of anchor nodes created by the +// browser (resolving and parsing URLs) that is unlikely to be provided by mock objects and +// cause us to break tests. In addition, when the browser resolves a URL for XHR, it +// 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 = window.document.createElement('a'); +var originUrl = urlResolve(window.location.href); + + +/** + * + * Implementation Notes for non-IE browsers + * ---------------------------------------- + * Assigning a URL to the href property of an anchor DOM node, even one attached to the DOM, + * results both in the normalizing and parsing of the URL. Normalizing means that a relative + * 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 + * + * Implementation Notes for IE + * --------------------------- + * 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. + * + * References: + * http://developer.mozilla.org/en-US/docs/Web/API/HTMLAnchorElement + * 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/ + * + * @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. + * + * | member name | Description | + * |---------------|----------------| + * | href | A normalized version of the provided URL if it was not an absolute URL | + * | protocol | The protocol including the trailing colon | + * | host | The host and port (if the port is non-default) of the normalizedUrl | + * | search | The search params, minus the question mark | + * | hash | The hash string, minus the hash symbol + * | hostname | The hostname + * | port | The port, without ":" + * | pathname | The pathname, beginning with "/" + * + */ +function urlResolve(url) { + var href = url; + + if (msie) { + // Normalize before parse. Refer Implementation Notes on why this is + // done in two steps on IE. + urlParsingNode.setAttribute('href', href); + href = urlParsingNode.href; + } + + urlParsingNode.setAttribute('href', href); + + // urlParsingNode provides the UrlUtils interface - http://url.spec.whatwg.org/#urlutils + return { + href: urlParsingNode.href, + protocol: urlParsingNode.protocol ? urlParsingNode.protocol.replace(/:$/, '') : '', + host: urlParsingNode.host, + search: urlParsingNode.search ? urlParsingNode.search.replace(/^\?/, '') : '', + hash: urlParsingNode.hash ? urlParsingNode.hash.replace(/^#/, '') : '', + hostname: urlParsingNode.hostname, + port: urlParsingNode.port, + pathname: (urlParsingNode.pathname.charAt(0) === '/') + ? urlParsingNode.pathname + : '/' + urlParsingNode.pathname + }; +} + +/** + * Parse a request URL and determine whether this is a same-origin request as the application document. + * + * @param {string|object} requestUrl The url of the request as a string that will be resolved + * or a parsed URL object. + * @returns {boolean} Whether the request is for the same origin as the application document. + */ +function urlIsSameOrigin(requestUrl) { + var parsed = (isString(requestUrl)) ? urlResolve(requestUrl) : requestUrl; + return (parsed.protocol === originUrl.protocol && + parsed.host === originUrl.host); +} + +/** + * @ngdoc service + * @name $window + * @this + * + * @description + * A reference to the browser's `window` object. While `window` + * is globally available in JavaScript, it causes testability problems, because + * it is a global variable. In angular we always refer to it through the + * `$window` service, so it may be overridden, removed or mocked for testing. + * + * Expressions, like the one defined for the `ngClick` directive in the example + * below, are evaluated with respect to the current scope. Therefore, there is + * no risk of inadvertently coding in a dependency on a global value in such an + * expression. + * + * @example + + + +
      + + +
      +
      + + it('should display the greeting in the input box', function() { + element(by.model('greeting')).sendKeys('Hello, E2E Tests'); + // If we click the button it will block the test runner + // element(':button').click(); + }); + +
      + */ +function $WindowProvider() { + this.$get = valueFn(window); +} + +/** + * @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 safeDecodeURIComponent(str) { + try { + return decodeURIComponent(str); + } catch (e) { + return str; + } + } + + return function() { + var cookieArray, cookie, i, index, name; + var currentCookieString = rawDocument.cookie || ''; + + 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) + * $provide.value('greet', function(name){ + * return 'Hello ' + name + '!'; + * }); + * + * // register a filter factory which uses the + * // greet service to demonstrate DI. + * $filterProvider.register('greet', function(greet){ + * // return the filter function which uses the greet service + * // to generate salutation + * return function(text) { + * // filters need to be forgiving so check input validity + * return text && greet(text) || text; + * }; + * }); + * } + * ``` + * + * 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(){ + * return ...; + * }); + * }, + * 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 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: + * + * ```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 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)) { + var filters = {}; + forEach(name, function(filter, key) { + filters[key] = register(key, filter); + }); + return filters; + } else { + return $provide.factory(name + suffix, factory); + } + } + this.register = register; + + this.$get = ['$injector', function($injector) { + return function(name) { + return $injector.get(name + suffix); + }; + }]; + + //////////////////////////////////////// + + /* global + currencyFilter: false, + dateFilter: false, + filterFilter: false, + jsonFilter: false, + limitToFilter: false, + lowercaseFilter: false, + numberFilter: false, + orderByFilter: false, + uppercaseFilter: false + */ + + register('currency', currencyFilter); + register('date', dateFilter); + register('filter', filterFilter); + register('json', jsonFilter); + register('limitTo', limitToFilter); + register('lowercase', lowercaseFilter); + register('number', numberFilter); + register('orderBy', orderByFilter); + register('uppercase', uppercaseFilter); +} + +/** + * @ngdoc filter + * @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. + * @param {string|Object|function()} expression The predicate to be used for selecting items from + * `array`. + * + * Can be one of: + * + * - `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 (`$` 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". + * + * 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'}`. + * + * - `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 the expected value (from the filter expression) and actual value (from + * the object in the array) should be considered a match. + * + * 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 both values should be considered equal. + * + * - `true`: A shorthand for `function(actual, expected) { return angular.equals(actual, expected)}`. + * This is essentially strict comparison of expected and actual. + * + * - `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 + + +
      + + + + + + + + +
      NamePhone
      {{friend.name}}{{friend.phone}}
      +
      +
      +
      +
      +
      + + + + + + +
      NamePhone
      {{friendObj.name}}{{friendObj.phone}}
      +
      + + 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]); + }); + }); + }; + + 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() { + 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() { + 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, anyPropertyKey) { + if (!isArrayLike(array)) { + if (array == null) { + return array; + } else { + throw minErr('filter')('notarray', 'Expected array but received: {0}', array); + } + } + + anyPropertyKey = anyPropertyKey || '$'; + var expressionType = getTypeForFilter(expression); + var predicateFn; + var matchAgainstAnyProp; + + switch (expressionType) { + case 'function': + 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; + } + + 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) { + if ((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 currency + * @kind function + * + * @description + * Formats a number as a currency (ie $1,234.56). When no currency symbol is provided, default + * symbol for current locale is used. + * + * @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$"}} + no fractions (0): {{amount | currency:"USD$":0}} +
      +
      + + it('should init with 1234.56', function() { + 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() { + 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, 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 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 `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 + + + +
      +
      + Default formatting: {{val | number}}
      + No fractions: {{val | number:0}}
      + Negative number: {{-val | number:4}} +
      +
      + + it('should format numbers', function() { + 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() { + 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) { + + // if null or undefined pass it through + return (number == null) + ? number + : formatNumber(number, formats.PATTERNS[0], formats.GROUP_SEP, formats.DECIMAL_SEP, + fractionSize); + }; +} + +/** + * 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; + + // Decimal point? + if ((numberOfIntegerDigits = numStr.indexOf(DECIMAL_SEP)) > -1) { + numStr = numStr.replace(DECIMAL_SEP, ''); + } + + // 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; + } + + // Count the number of leading zeros. + for (i = 0; numStr.charAt(i) === ZERO_CHAR; i++) { /* empty */ } + + 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--; + + // 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); + } + } + + // 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 }; +} + +/** + * 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 || (negWrap && num <= 0)) { + if (negWrap) { + num = -num + 1; + } else { + num = -num; + neg = '-'; + } + } + num = '' + num; + 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, negWrap) { + offset = offset || 0; + return function(date) { + var value = date['get' + name](); + if (offset > 0 || value > -offset) { + value += offset; + } + if (value === 0 && offset === -12) value = 12; + return padNumber(value, size, trim, negWrap); + }; +} + +function dateStrGetter(name, shortForm, standAlone) { + return function(date, formats) { + var value = date['get' + name](); + var propPrefix = (standAlone ? 'STANDALONE' : '') + (shortForm ? 'SHORT' : ''); + var get = uppercase(propPrefix + name); + + return formats[get][value]; + }; +} + +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); + + 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, 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), + H: dateGetter('Hours', 1), + hh: dateGetter('Hours', 2, -12), + h: dateGetter('Hours', 1, -12), + mm: dateGetter('Minutes', 2), + m: dateGetter('Minutes', 1), + ss: dateGetter('Seconds', 2), + s: dateGetter('Seconds', 1), + // while ISO 8601 requires fractions to be prefixed with `.` or `,` + // we can be just safely rely on using `sss` since we currently don't support single or two digit fractions + sss: dateGetter('Milliseconds', 3), + EEEE: dateStrGetter('Day'), + EEE: dateStrGetter('Day', true), + a: ampmGetter, + Z: timeZoneGetter, + ww: weekGetter(2), + w: weekGetter(1), + G: eraGetter, + GG: eraGetter, + GGG: eraGetter, + GGGG: longEraGetter +}; + +var DATE_FORMATS_SPLIT = /((?:[^yMLdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|L+|d+|H+|h+|m+|s+|a|Z|G+|w+))(.*)/, + NUMBER_STRING = /^-?\d+$/; + +/** + * @ngdoc filter + * @name date + * @kind function + * + * @description + * Formats `date` to a string based on the requested `format`. + * + * `format` string can be composed of the following elements: + * + * * `'yyyy'`: 4 digit representation of year (e.g. AD 1 => 0001, AD 2010 => 2010) + * * `'yy'`: 2 digit representation of year, padded (00-99). (e.g. AD 2001 => 01, AD 2010 => 10) + * * `'y'`: 1 digit representation of year, e.g. (AD 1 => 1, AD 199 => 199) + * * `'MMMM'`: Month in year (January-December) + * * `'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) + * * `'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'`: 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. 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) + * + * `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'"`). + * + * @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 + * 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:'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 'at' h:mma"}}: + {{'1288323623006' | date:"MM/dd/yyyy 'at' h:mma"}}
      +
      + + it('should format date', function() { + expect(element(by.binding("1288323623006 | date:'medium'")).getText()). + toMatch(/Oct 2\d, 2010 \d{1,2}:\d{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) { + + + var R_ISO8601_STR = /^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(Z|([+-])(\d\d):?(\d\d))?)?$/; + // 1 2 3 4 5 6 7 8 9 10 11 + function jsonStringToDate(string) { + var match; + 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; + + if (match[9]) { + tzHour = toInt(match[9] + match[10]); + tzMin = toInt(match[9] + match[11]); + } + 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; + } + return string; + } + + + return function(date, format, timezone) { + var text = '', + parts = [], + fn, match; + + format = format || 'mediumDate'; + format = $locale.DATETIME_FORMATS[format] || format; + if (isString(date)) { + date = NUMBER_STRING.test(date) ? toInt(date) : jsonStringToDate(date); + } + + if (isNumber(date)) { + date = new Date(date); + } + + if (!isDate(date) || !isFinite(date.getTime())) { + return date; + } + + while (format) { + match = DATE_FORMATS_SPLIT.exec(format); + if (match) { + parts = concat(parts, match, 1); + format = parts.pop(); + } else { + parts.push(format); + format = null; + } + } + + 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, dateTimezoneOffset) + : value === '\'\'' ? '\'' : value.replace(/(^'|'$)/g, '').replace(/''/g, '\''); + }); + + return text; + }; +} + + +/** + * @ngdoc filter + * @name json + * @kind function + * + * @description + * Allows you to convert a JavaScript object into JSON string. + * + * This filter is mostly useful for debugging. When using the double curly {{value}} notation + * 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 }}
      +
      {{ {'name':'value'} | json:4 }}
      +
      + + it('should jsonify filtered objects', function() { + 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, spacing) { + if (isUndefined(spacing)) { + spacing = 2; + } + return toJson(object, spacing); + }; +} + + +/** + * @ngdoc filter + * @name lowercase + * @kind function + * @description + * Converts string to lowercase. + * @see angular.lowercase + */ +var lowercaseFilter = valueFn(lowercase); + + +/** + * @ngdoc filter + * @name uppercase + * @kind function + * @description + * Converts string to uppercase. + * @see angular.uppercase + */ +var uppercaseFilter = valueFn(uppercase); + +/** + * @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, 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|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`. 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 + + + +
      + +

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

      + +

      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(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'); + }); + + // 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() { + 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, begin) { + if (Math.abs(Number(limit)) === Infinity) { + limit = Number(limit); + } else { + limit = toInt(limit); + } + if (isNumberNaN(limit)) return input; + + if (isNumber(input)) input = input.toString(); + if (!isArrayLike(input)) return input; + + 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 sliceFn(input, Math.max(0, begin + limit), begin); + } + } + }; +} + +function sliceFn(input, begin, end) { + if (isString(input)) return input.slice(begin, end); + + return slice.call(input, begin, end); +} + +/** + * @ngdoc filter + * @name orderBy + * @kind function + * + * @description + * Returns an array containing the items from the specified `collection`, ordered by a `comparator` + * function based on the values computed using the `expression` predicate. + * + * 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.) + * + * 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`: 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. + * + * + * @example + * ### 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. + * + + +
      + + + + + + + + + + + +
      NamePhone NumberAge
      {{friend.name}}{{friend.phone}}{{friend.age}}
      +
      +
      + + 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 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 `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, compareFn) { + + if (array == null) return array; + if (!isArrayLike(array)) { + throw minErr('orderBy')('notarray', 'Expected array but received: {0}', array); + } + + 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 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; + } + } + + return compare(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) { + if (isFunction(directive)) { + directive = { + link: directive + }; + } + directive.restrict = directive.restrict || 'AC'; + return valueFn(directive); +} + +/** + * @ngdoc directive + * @name a + * @restrict E + * + * @description + * Modifies the default behavior of the html a tag so that the default action is prevented when + * the href attribute is empty. + * + * 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 (!attr.href && !attr.xlinkHref) { + return function(scope, element) { + // 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)) { + event.preventDefault(); + } + }); + }; + } + } +}); + +/** + * @ngdoc directive + * @name ngHref + * @restrict A + * @priority 99 + * + * @description + * Using Angular markup like `{{hash}}` in an href attribute will + * 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. + * + * 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. + * + * @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)
      + link 3 (link, reload!)
      + 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(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(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(by.id('link-3')).getAttribute('href')).toMatch(/\/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(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(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() { + element(by.model('value')).clear(); + element(by.model('value')).sendKeys('6'); + expect(element(by.id('link-6')).getAttribute('href')).toMatch(/\/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 ngSrc + * @restrict A + * @priority 99 + * + * @description + * Using Angular markup like `{{hash}}` in a `src` attribute doesn't + * work right: The browser will fetch from the URL with the literal + * text `{{hash}}` until Angular replaces the expression inside + * `{{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. + */ + +/** + * @ngdoc directive + * @name ngSrcset + * @restrict A + * @priority 99 + * + * @description + * Using Angular markup like `{{hash}}` in a `srcset` attribute doesn't + * work right: The browser will fetch from the URL with the literal + * text `{{hash}}` until Angular replaces the expression inside + * `{{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. + */ + +/** + * @ngdoc directive + * @name ngDisabled + * @restrict A + * @priority 100 + * + * @description + * + * This directive sets the `disabled` attribute on the element if the + * {@link guide/expression expression} inside `ngDisabled` evaluates to truthy. + * + * 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 + + +
      + +
      + + it('should toggle button', function() { + 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 the `disabled` attribute will be set on the element + */ + + +/** + * @ngdoc directive + * @name ngChecked + * @restrict A + * @priority 100 + * + * @description + * 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 + + +
      + +
      + + it('should check both checkBoxes', function() { + 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 the `checked` attribute will be set on the element + */ + + +/** + * @ngdoc directive + * @name ngReadonly + * @restrict A + * @priority 100 + * + * @description + * + * 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 + + +
      + +
      + + it('should toggle readonly attr', function() { + 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, + * then special attribute "readonly" will be set on the element + */ + + +/** + * @ngdoc directive + * @name ngSelected + * @restrict A + * @priority 100 + * + * @description + * + * 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 + + +
      + +
      + + it('should select Greetings!', function() { + 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, + * then special attribute "selected" will be set on the element + */ + +/** + * @ngdoc directive + * @name ngOpen + * @restrict A + * @priority 100 + * + * @description + * + * 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 + * + * Edge, Firefox, and Internet Explorer do not support the `details` element, it is + * recommended to use {@link ng.ngShow} and {@link ng.ngHide} instead. + * + * @example + + +
      +
      + Show/Hide me +
      +
      + + it('should toggle open', function() { + 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, + * then special attribute "open" will be set on the element + */ + +var ngAttributeAliasDirectives = {}; + +// boolean attrs are evaluated +forEach(BOOLEAN_ATTR, function(propName, attrName) { + // binding to multiple is not supported + 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) { + //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); + ngAttributeAliasDirectives[normalized] = function() { + 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 (attrName === 'href') { + attr.$set(name, null); + } + return; + } + + attr.$set(name, value); + + // 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 && propName) element.prop(propName, attr[name]); + }); + } + }; + }; +}); + +/* global -nullFormCtrl, -SUBMITTED_CLASS, addSetValidityMethod: true + */ +var nullFormCtrl = { + $addControl: noop, + $$renameControl: nullFormRenameControl, + $removeControl: noop, + $setValidity: noop, + $setDirty: noop, + $setPristine: noop, + $setSubmitted: noop +}, +SUBMITTED_CLASS = 'ng-submitted'; + +function nullFormRenameControl(control, name) { + control.$name = name; +} + +/** + * @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} $pending True if at least one containing control or form is pending. + * @property {boolean} $submitted True if user has submitted the form even if its invalid. + * + * @property {Object} $error Is 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 have a failing validator for given error name. + * + * Built-in validation tokens: + * + * - `email` + * - `max` + * - `maxlength` + * - `min` + * - `minlength` + * - `number` + * - `pattern` + * - `required` + * - `url` + * - `date` + * - `datetimelocal` + * - `time` + * - `week` + * - `month` + * + * @description + * `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 + * of `FormController`. + * + */ +//asks for $scope to fool the BC controller module +FormController.$inject = ['$element', '$attrs', '$scope', '$animate', '$interpolate']; +function FormController(element, attrs, $scope, $animate, $interpolate) { + var form = this, + controls = []; + + // init state + form.$error = {}; + form.$$success = {}; + form.$pending = undefined; + form.$name = $interpolate(attrs.name || attrs.ngForm || '')($scope); + form.$dirty = false; + form.$pristine = true; + form.$valid = true; + form.$invalid = false; + form.$submitted = false; + form.$$parentForm = nullFormCtrl; + + /** + * @ngdoc method + * @name form.FormController#$rollbackViewValue + * + * @description + * Rollback 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 typically needed by the reset button of + * a form that uses `ng-model-options` to pend updates. + */ + form.$rollbackViewValue = function() { + forEach(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. + */ + form.$commitViewValue = function() { + forEach(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. + */ + 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); + + if (control.$name) { + form[control.$name] = control; + } + + control.$$parentForm = form; + }; + + // Private API: rename a form control + form.$$renameControl = function(control, newName) { + var oldName = control.$name; + + if (form[oldName] === control) { + delete form[oldName]; + } + form[newName] = control; + control.$name = newName; + }; + + /** + * @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]; + } + forEach(form.$pending, function(value, name) { + form.$setValidity(name, null, control); + }); + forEach(form.$error, function(value, name) { + form.$setValidity(name, null, control); + }); + forEach(form.$$success, function(value, name) { + form.$setValidity(name, null, control); + }); + + arrayRemove(controls, control); + control.$$parentForm = nullFormCtrl; + }; + + + /** + * @ngdoc method + * @name form.FormController#$setValidity + * + * @description + * Sets the validity of a form control. + * + * This method will also propagate to parent forms. + */ + addSetValidityMethod({ + ctrl: this, + $element: element, + 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]; + } + }, + $animate: $animate + }); + + /** + * @ngdoc method + * @name form.FormController#$setDirty + * + * @description + * Sets the form to a dirty state. + * + * 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() { + $animate.removeClass(element, PRISTINE_CLASS); + $animate.addClass(element, DIRTY_CLASS); + form.$dirty = true; + form.$pristine = false; + form.$$parentForm.$setDirty(); + }; + + /** + * @ngdoc method + * @name form.FormController#$setPristine + * + * @description + * Sets the form to its pristine state. + * + * 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() { + $animate.setClass(element, PRISTINE_CLASS, DIRTY_CLASS + ' ' + SUBMITTED_CLASS); + form.$dirty = false; + form.$pristine = true; + form.$submitted = false; + forEach(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. + */ + form.$setUntouched = function() { + forEach(controls, function(control) { + control.$setUntouched(); + }); + }; + + /** + * @ngdoc method + * @name form.FormController#$setSubmitted + * + * @description + * Sets the form to its submitted state. + */ + form.$setSubmitted = function() { + $animate.addClass(element, SUBMITTED_CLASS); + form.$submitted = true; + form.$$parentForm.$setSubmitted(); + }; +} + +/** + * @ngdoc directive + * @name ngForm + * @restrict EAC + * + * @description + * Nestable alias of {@link ng.directive:form `form`} directive. HTML + * 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. + * + */ + + /** + * @ngdoc directive + * @name form + * @restrict E + * + * @description + * Directive that instantiates + * {@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 + * 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 + * `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 + * + * Since the role of forms in client-side Angular applications is different than in classical + * roundtrip apps, it is desirable for the browser not to translate the form submission into a full + * page reload that sends the data to the server. Instead some javascript logic should be triggered + * to handle the form submission in an application-specific way. + * + * For this reason, Angular prevents the default action (form submission to the server) unless the + * `` element has an `action` attribute specified. + * + * You can use one of the following two ways to specify what javascript method should be called when + * a form is submitted: + * + * - {@link ng.directive:ngSubmit ngSubmit} directive on the form element + * - {@link ng.directive:ngClick ngClick} directive on the first + * button or input field of type submit (input[type=submit]) + * + * To prevent double execution of the handler, use only one of the {@link ng.directive:ngSubmit ngSubmit} + * or {@link ng.directive:ngClick ngClick} directives. + * This is because of the following form submission rules in the HTML specification: + * + * - If a form has only one input field then hitting enter in this field triggers form submit + * (`ngSubmit`) + * - if a form has 2+ input fields and no buttons or input[type=submit] then hitting enter + * doesn't trigger submit + * - if a form has one or more input fields and one or more buttons or input[type=submit] then + * 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`) + * + * 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}}
      + +
      + + it('should initialize to model', function() { + 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() { + 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', '$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 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 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 handleFormSubmission = function(event) { + scope.$apply(function() { + controller.$commitViewValue(); + controller.$setSubmitted(); + }); + + event.preventDefault(); + }; + + addEventListenerFn(formElement[0], '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', handleFormSubmission); + }, 0, false); + }); + } + + var parentFormCtrl = ctrls[1] || controller.$$parentForm; + parentFormCtrl.$addControl(controller); + + 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); + +/* global + VALID_CLASS: false, + INVALID_CLASS: false, + PRISTINE_CLASS: false, + DIRTY_CLASS: false, + ngModelMinErr: false +*/ + +// 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 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 input + * @name input[text] + * + * @description + * 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. + * @param {string=} required Adds `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 {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. 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 + + + +
      + +
      + + 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')); + + 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(value.getText()).toContain('2013-10-22'); + 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-01'); + expect(value.getText()).toContain(''); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); + +
      + */ + 'date': createDateInputType('date', DATE_REGEXP, + createDateParser(DATE_REGEXP, ['yyyy', 'MM', 'dd']), + 'yyyy-MM-dd'), + + /** + * @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`. + * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. + * @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 {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. 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 + + + +
      + +
      + + Required! + + Not valid number! +
      + 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(value.getText()).toContain('12'); + expect(valid.getText()).toContain('true'); + }); + + it('should be invalid if empty', function() { + input.clear(); + input.sendKeys(''); + expect(value.getText()).toEqual('value ='); + expect(valid.getText()).toContain('false'); + }); + + it('should be invalid if over max', function() { + input.clear(); + input.sendKeys('123'); + expect(value.getText()).toEqual('value ='); + expect(valid.getText()).toContain('false'); + }); + +
      + */ + 'number': numberInputType, + + + /** + * @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. + * @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 {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. 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 + + + +
      +
      + + 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(text.getText()).toContain('http://google.com'); + 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 not url', function() { + input.clear(); + input.sendKeys('box'); + + expect(valid.getText()).toContain('false'); + }); + +
      + */ + 'url': urlInputType, + + + /** + * @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. + * @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 {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. 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 + + + +
      + +
      + + Required! + + Not valid email! +
      + 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(text.getText()).toContain('me@example.com'); + 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 not email', function() { + input.clear(); + input.sendKeys('xxx'); + + expect(valid.getText()).toContain('false'); + }); + +
      + */ + 'email': emailInputType, + + + /** + * @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 `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 + + + +
      +
      +
      +
      + 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() { + var inputs = element.all(by.model('color.name')); + var color = element(by.binding('color.name')); + + 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. + * + *
      + *

      + * In v1.5.9+, in order to avoid interfering with already existing, custom directives for + * `input[range]`, you need to let Angular know that you want to enable its built-in support. + * You can do this by adding the `ng-input-range` attribute to the input element. E.g.: + * `` + *


      + *

      + * Input elements without the `ng-input-range` attibute will continue to be treated the same + * as in previous versions (e.g. their model value will be a string not a number and Angular + * will not take `min`/`max`/`step` attributes and properties into account). + *


      + *

      + * **Note:** From v1.6.x onwards, the support for `input[range]` will be always enabled and + * the `ng-input-range` attribute will have no effect. + *


      + *

      + * This documentation page refers to elements which have the built-in support enabled; i.e. + * elements _with_ the `ng-input-range` attribute. + *

      + *
      + * + * 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 ngInputRange The presense of this attribute enables the built-in support for + * `input[range]`. + * @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 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 {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 = {{checkboxModel.value1}}
      + value2 = {{checkboxModel.value2}}
      +
      +
      + + it('should change state', function() { + var value1 = element(by.binding('checkboxModel.value1')); + var value2 = element(by.binding('checkboxModel.value2')); + + 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, + 'file': noop +}; + +function stringBasedInputType(ctrl) { + ctrl.$formatters.push(function(value) { + return ctrl.$isEmpty(value) ? value : value.toString(); + }); +} + +function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { + 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() { + composing = true; + }); + + element.on('compositionend', function() { + composing = false; + listener(); + }); + } + + var timeout; + + var listener = function(ev) { + if (timeout) { + $browser.defer.cancel(timeout); + timeout = null; + } + if (composing) return; + 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 + // If input type is 'password', the value is never trimmed + if (type !== 'password' && (!attr.ngTrim || attr.ngTrim !== 'false')) { + value = trim(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); + } + }; + + // if the browser does support "input" event, we are fine - except on IE9 which doesn't fire the + // input event on backspace, delete or cut + if ($sniffer.hasEvent('input')) { + element.on('input', listener); + } else { + var deferListener = function(ev, input, origValue) { + if (!timeout) { + timeout = $browser.defer(function() { + timeout = null; + if (!input || input.value !== origValue) { + listener(ev); + } + }); + } + }; + + 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(event, this, this.value); + }); + + // if user modifies input value using context menu in IE, we need "paste" and "cut" events to catch it + if ($sniffer.hasEvent('paste')) { + element.on('paste cut', deferListener); + } + } + + // if user paste into input using mouse on older browser + // 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() { + // Workaround for Firefox validation #12102. + var value = ctrl.$isEmpty(ctrl.$viewValue) ? '' : ctrl.$viewValue; + if (element.val() !== value) { + element.val(value); + } + }; +} + +function weekParser(isoWeek, existingDate) { + if (isDate(isoWeek)) { + return isoWeek; + } + + 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 (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; + } + + 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 && ctrl.$options.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; + } + + return numString.length - decimalSymbolIndex - 1; +} + +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); + + // 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 (!isNumberInteger(value) || !isNumberInteger(stepBase) || !isNumberInteger(step)) { + var decimalCount = Math.max(countDecimals(value), countDecimals(stepBase), countDecimals(step)); + var multiplier = Math.pow(10, decimalCount); + + value = value * multiplier; + stepBase = stepBase * multiplier; + step = step * multiplier; + } + + return (value - stepBase) % step === 0; +} + +function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { + badInputChecker(scope, element, attr, ctrl); + baseInputType(scope, element, attr, ctrl, $sniffer, $browser); + numberFormatterParser(ctrl); + + 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(); + }); + } +} + +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; + } + + 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(); + } + } + + function maxChange(val) { + maxVal = parseNumberAttrVal(val); + // ignore changes before model is initialized + if (isNumberNaN(ctrl.$modelValue)) { + return; + } + + 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(); + } + } + + 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) { + // 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); + + ctrl.$$parserName = 'url'; + ctrl.$validators.url = function(modelValue, viewValue) { + var value = modelValue || viewValue; + return ctrl.$isEmpty(value) || URL_REGEXP.test(value); + }; +} + +function emailInputType(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); + + ctrl.$$parserName = 'email'; + ctrl.$validators.email = function(modelValue, viewValue) { + var value = modelValue || viewValue; + return ctrl.$isEmpty(value) || EMAIL_REGEXP.test(value); + }; +} + +function radioInputType(scope, element, attr, ctrl) { + // make the name unique, if not defined + if (isUndefined(attr.name)) { + element.attr('name', nextUid()); + } + + var listener = function(ev) { + if (element[0].checked) { + ctrl.$setViewValue(attr.value, ev && ev.type); + } + }; + + element.on('click', listener); + + ctrl.$render = function() { + var value = attr.value; + // We generally use strict comparison. This is behavior we cannot change without a BC. + // eslint-disable-next-line eqeqeq + element[0].checked = (value == ctrl.$viewValue); + }; + + attr.$observe('value', ctrl.$render); +} + +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; +} + +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); + + 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 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 === false; + }; + + ctrl.$formatters.push(function(value) { + return equals(value, trueValue); + }); + + ctrl.$parsers.push(function(value) { + return value ? trueValue : falseValue; + }); +} + + +/** + * @ngdoc directive + * @name textarea + * @restrict E + * + * @description + * HTML textarea element control with angular data-binding. The data-binding and validation + * properties of this element are exactly the same as those of the + * {@link ng.directive:input input element}. + * + * @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. + * @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 {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. 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 ` + *
      {{ 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) { + // We want to control whitespace trimming so we use this convoluted approach + // to access the ngList attribute, which doesn't pre-trim the attribute + var ngList = element.attr(attr.$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 +*/ + +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', + PENDING_CLASS = 'ng-pending', + 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 reads value from the DOM. 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`. + + * + * @property {Array.} $formatters Array of functions to execute, as a pipeline, whenever + the model value changes. 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. + Used to format / convert values for display in the control. + * ```js + * function formatter(value) { + * if (value) { + * return value.toUpperCase(); + * } + * } + * ngModel.$formatters.push(formatter); + * ``` + * + * @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/); + }); + + *
      + * + * + */ +var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', '$animate', '$timeout', '$rootScope', '$q', '$interpolate', + /** @this */ function($scope, $exceptionHandler, $attr, $element, $parse, $animate, $timeout, $rootScope, $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; + + var parsedNgModel = $parse($attr.ngModel), + parsedNgModelAssign = parsedNgModel.assign, + ngModelGet = parsedNgModel, + ngModelSet = parsedNgModelAssign, + pendingDebounce = null, + parserValid, + ctrl = this; + + this.$$setOptions = function(options) { + ctrl.$options = options; + if (options && options.getterSetter) { + var invokeModelGetter = $parse($attr.ngModel + '()'), + invokeModelSetter = $parse($attr.ngModel + '($$$p)'); + + ngModelGet = function($scope) { + var modelValue = parsedNgModel($scope); + if (isFunction(modelValue)) { + modelValue = invokeModelGetter($scope); + } + return modelValue; + }; + ngModelSet = function($scope, newValue) { + if (isFunction(parsedNgModel($scope))) { + invokeModelSetter($scope, {$$$p: newValue}); + } else { + parsedNgModelAssign($scope, newValue); + } + }; + } else if (!parsedNgModel.assign) { + throw ngModelMinErr('nonassign', 'Expression \'{0}\' is non-assignable. Element: {1}', + $attr.ngModel, startingTag($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. + */ + this.$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". + */ + this.$isEmpty = function(value) { + // eslint-disable-next-line no-self-compare + return isUndefined(value) || value === '' || value === null || value !== value; + }; + + this.$$updateEmptyClasses = function(value) { + if (ctrl.$isEmpty(value)) { + $animate.removeClass($element, NOT_EMPTY_CLASS); + $animate.addClass($element, EMPTY_CLASS); + } else { + $animate.removeClass($element, EMPTY_CLASS); + $animate.addClass($element, NOT_EMPTY_CLASS); + } + }; + + + var currentValidationRunId = 0; + + /** + * @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` + * class 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({ + ctrl: this, + $element: $element, + set: function(object, property) { + object[property] = true; + }, + unset: function(object, property) { + delete object[property]; + }, + $animate: $animate + }); + + /** + * @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. + */ + this.$setPristine = function() { + ctrl.$dirty = false; + ctrl.$pristine = true; + $animate.removeClass($element, DIRTY_CLASS); + $animate.addClass($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. + */ + this.$setDirty = function() { + ctrl.$dirty = true; + ctrl.$pristine = false; + $animate.removeClass($element, PRISTINE_CLASS); + $animate.addClass($element, DIRTY_CLASS); + ctrl.$$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. + */ + this.$setUntouched = function() { + ctrl.$touched = false; + ctrl.$untouched = true; + $animate.setClass($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). + */ + this.$setTouched = function() { + ctrl.$touched = true; + ctrl.$untouched = false; + $animate.setClass($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; + } + + + *
      + */ + this.$rollbackViewValue = function() { + $timeout.cancel(pendingDebounce); + ctrl.$viewValue = ctrl.$$lastCommittedViewValue; + ctrl.$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. + */ + this.$validate = function() { + // ignore $validate before model is initialized + if (isNumberNaN(ctrl.$modelValue)) { + return; + } + + var viewValue = ctrl.$$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 = ctrl.$$rawModelValue; + + var prevValid = ctrl.$valid; + var prevModelValue = ctrl.$modelValue; + + var allowInvalid = ctrl.$options && ctrl.$options.allowInvalid; + + ctrl.$$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 ctrl.$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. + ctrl.$modelValue = allValid ? modelValue : undefined; + + if (ctrl.$modelValue !== prevModelValue) { + ctrl.$$writeModelToScope(); + } + } + }); + + }; + + this.$$runValidators = function(modelValue, viewValue, doneCallback) { + currentValidationRunId++; + var localValidationRunId = currentValidationRunId; + + // check parser error + if (!processParseErrors()) { + validationDone(false); + return; + } + if (!processSyncValidators()) { + validationDone(false); + return; + } + processAsyncValidators(); + + function processParseErrors() { + var errorKey = ctrl.$$parserName || 'parse'; + if (isUndefined(parserValid)) { + setValidity(errorKey, null); + } else { + if (!parserValid) { + forEach(ctrl.$validators, function(v, name) { + setValidity(name, null); + }); + forEach(ctrl.$asyncValidators, function(v, name) { + setValidity(name, null); + }); + } + // Set the parse error last, to prevent unsetting it, should a $validators key == parserName + setValidity(errorKey, parserValid); + return parserValid; + } + return true; + } + + function processSyncValidators() { + var syncValidatorsValid = true; + forEach(ctrl.$validators, function(validator, name) { + var result = validator(modelValue, viewValue); + syncValidatorsValid = syncValidatorsValid && result; + setValidity(name, result); + }); + if (!syncValidatorsValid) { + forEach(ctrl.$asyncValidators, function(v, name) { + setValidity(name, null); + }); + return false; + } + return true; + } + + function processAsyncValidators() { + var validatorPromises = []; + var allValid = true; + forEach(ctrl.$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 { + $q.all(validatorPromises).then(function() { + validationDone(allValid); + }, noop); + } + } + + function setValidity(name, isValid) { + if (localValidationRunId === currentValidationRunId) { + ctrl.$setValidity(name, isValid); + } + } + + function validationDone(allValid) { + if (localValidationRunId === 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. + */ + this.$commitViewValue = function() { + var viewValue = ctrl.$viewValue; + + $timeout.cancel(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 (ctrl.$$lastCommittedViewValue === viewValue && (viewValue !== '' || !ctrl.$$hasNativeValidators)) { + return; + } + ctrl.$$updateEmptyClasses(viewValue); + ctrl.$$lastCommittedViewValue = viewValue; + + // change to dirty + if (ctrl.$pristine) { + this.$setDirty(); + } + this.$$parseAndValidate(); + }; + + this.$$parseAndValidate = function() { + var viewValue = ctrl.$$lastCommittedViewValue; + var modelValue = viewValue; + parserValid = isUndefined(modelValue) ? undefined : true; + + if (parserValid) { + for (var i = 0; i < ctrl.$parsers.length; i++) { + modelValue = ctrl.$parsers[i](modelValue); + if (isUndefined(modelValue)) { + parserValid = false; + break; + } + } + } + if (isNumberNaN(ctrl.$modelValue)) { + // ctrl.$modelValue has not been touched yet... + ctrl.$modelValue = ngModelGet($scope); + } + var prevModelValue = ctrl.$modelValue; + var allowInvalid = ctrl.$options && ctrl.$options.allowInvalid; + ctrl.$$rawModelValue = modelValue; + + if (allowInvalid) { + ctrl.$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 + ctrl.$$runValidators(modelValue, ctrl.$$lastCommittedViewValue, function(allValid) { + if (!allowInvalid) { + // Note: Don't check ctrl.$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. + ctrl.$modelValue = allValid ? modelValue : undefined; + writeToModelIfNeeded(); + } + }); + + function writeToModelIfNeeded() { + if (ctrl.$modelValue !== prevModelValue) { + ctrl.$$writeModelToScope(); + } + } + }; + + this.$$writeModelToScope = function() { + ngModelSet($scope, ctrl.$modelValue); + forEach(ctrl.$viewChangeListeners, function(listener) { + try { + listener(); + } catch (e) { + $exceptionHandler(e); + } + }); + }; + + /** + * @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 sent directly for processing, finally to be applied to `$modelValue` and then the + * **expression** specified in the `ng-model` attribute. Lastly, 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. + */ + this.$setViewValue = function(value, trigger) { + ctrl.$viewValue = value; + if (!ctrl.$options || ctrl.$options.updateOnDefault) { + ctrl.$$debounceViewValueCommit(trigger); + } + }; + + this.$$debounceViewValueCommit = function(trigger) { + var debounceDelay = 0, + options = ctrl.$options, + debounce; + + if (options && isDefined(options.debounce)) { + debounce = options.debounce; + if (isNumber(debounce)) { + debounceDelay = debounce; + } else if (isNumber(debounce[trigger])) { + debounceDelay = debounce[trigger]; + } else if (isNumber(debounce['default'])) { + debounceDelay = debounce['default']; + } + } + + $timeout.cancel(pendingDebounce); + if (debounceDelay) { + pendingDebounce = $timeout(function() { + ctrl.$commitViewValue(); + }, debounceDelay); + } else if ($rootScope.$$phase) { + ctrl.$commitViewValue(); + } else { + $scope.$apply(function() { + ctrl.$commitViewValue(); + }); + } + }; + + // 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' + $scope.$watch(function ngModelWatch() { + var modelValue = 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; + 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 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; + + modelCtrl.$$setOptions(ctrls[2] && ctrls[2].$options); + + // 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 && modelCtrl.$options.updateOn) { + element.on(modelCtrl.$options.updateOn, function(ev) { + modelCtrl.$$debounceViewValueCommit(ev && ev.type); + }); + } + + element.on('blur', function() { + if (modelCtrl.$touched) return; + + if ($rootScope.$$phase) { + scope.$evalAsync(modelCtrl.$setTouched); + } else { + scope.$apply(modelCtrl.$setTouched); + } + }); + } + }; + } + }; +}]; + + + +var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/; + +/** + * @ngdoc directive + * @name ngModelOptions + * + * @description + * Allows tuning how model updates are done. Using `ngModelOptions` you can 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. + * + * `ngModelOptions` has an effect on the element it's declared on and its descendants. + * + * @param {Object} ngModelOptions options to apply to the current model. 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 of 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. + * + * @example + + 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 = 
      +
      user.data = 
      +
      +
      + + angular.module('optionsExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.user = { name: 'John', 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(' Doe'); + input.click(); + expect(model.getText()).toEqual('John'); + other.click(); + expect(model.getText()).toEqual('John Doe'); + }); + + it('should $rollbackViewValue when model changes', function() { + input.sendKeys(' Doe'); + expect(input.getAttribute('value')).toEqual('John Doe'); + input.sendKeys(protractor.Key.ESCAPE); + expect(input.getAttribute('value')).toEqual('John'); + other.click(); + expect(model.getText()).toEqual('John'); + }); + +
      + + This one 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. + + + +
      +
      + + +
      +
      +
      user.name = 
      +
      +
      + + angular.module('optionsExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.user = { name: 'Igor' }; + }]); + +
      + + This one 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) { + // 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 ngModelOptionsDirective = function() { + return { + restrict: 'A', + controller: ['$scope', '$attrs', function NgModelOptionsController($scope, $attrs) { + var that = this; + this.$options = copy($scope.$eval($attrs.ngModelOptions)); + // Allow adding/overriding bound events + if (isDefined(this.$options.updateOn)) { + this.$options.updateOnDefault = false; + // extract "default" pseudo-event from list of events that can trigger a model update + this.$options.updateOn = trim(this.$options.updateOn.replace(DEFAULT_REGEXP, function() { + that.$options.updateOnDefault = true; + return ' '; + })); + } else { + this.$options.updateOnDefault = true; + } + }] + }; +}; + + + +// helper methods +function addSetValidityMethod(context) { + var ctrl = context.ctrl, + $element = context.$element, + classCache = {}, + set = context.set, + unset = context.unset, + $animate = context.$animate; + + classCache[INVALID_CLASS] = !(classCache[VALID_CLASS] = $element.hasClass(VALID_CLASS)); + + ctrl.$setValidity = setValidity; + + function setValidity(validationErrorKey, state, controller) { + if (isUndefined(state)) { + createAndSet('$pending', validationErrorKey, controller); + } else { + unsetAndCleanup('$pending', validationErrorKey, controller); + } + if (!isBoolean(state)) { + unset(ctrl.$error, validationErrorKey, controller); + unset(ctrl.$$success, validationErrorKey, controller); + } else { + if (state) { + unset(ctrl.$error, validationErrorKey, controller); + set(ctrl.$$success, validationErrorKey, controller); + } else { + set(ctrl.$error, validationErrorKey, controller); + unset(ctrl.$$success, validationErrorKey, controller); + } + } + if (ctrl.$pending) { + cachedToggleClass(PENDING_CLASS, true); + ctrl.$valid = ctrl.$invalid = undefined; + toggleValidationCss('', null); + } else { + cachedToggleClass(PENDING_CLASS, false); + ctrl.$valid = isObjectEmpty(ctrl.$error); + ctrl.$invalid = !ctrl.$valid; + toggleValidationCss('', ctrl.$valid); + } + + // re-read the state as the set/unset methods could have + // combined state in ctrl.$error[validationError] (used for forms), + // where setting/unsetting only increments/decrements the value, + // and does not replace it. + var combinedState; + if (ctrl.$pending && ctrl.$pending[validationErrorKey]) { + combinedState = undefined; + } else if (ctrl.$error[validationErrorKey]) { + combinedState = false; + } else if (ctrl.$$success[validationErrorKey]) { + combinedState = true; + } else { + combinedState = null; + } + + toggleValidationCss(validationErrorKey, combinedState); + ctrl.$$parentForm.$setValidity(validationErrorKey, combinedState, ctrl); + } + + function createAndSet(name, value, controller) { + if (!ctrl[name]) { + ctrl[name] = {}; + } + set(ctrl[name], value, controller); + } + + function unsetAndCleanup(name, value, controller) { + if (ctrl[name]) { + unset(ctrl[name], value, controller); + } + if (isObjectEmpty(ctrl[name])) { + ctrl[name] = undefined; + } + } + + function cachedToggleClass(className, switchValue) { + if (switchValue && !classCache[className]) { + $animate.addClass($element, className); + classCache[className] = true; + } else if (!switchValue && classCache[className]) { + $animate.removeClass($element, className); + classCache[className] = false; + } + } + + function toggleValidationCss(validationErrorKey, isValid) { + validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : ''; + + cachedToggleClass(VALID_CLASS + validationErrorKey, isValid === true); + cachedToggleClass(INVALID_CLASS + validationErrorKey, isValid === false); + } +} + +function isObjectEmpty(obj) { + if (obj) { + for (var prop in obj) { + if (obj.hasOwnProperty(prop)) { + return false; + } + } + } + return true; +} + +/** + * @ngdoc directive + * @name ngNonBindable + * @restrict AC + * @priority 1000 + * + * @description + * The `ngNonBindable` directive tells Angular not to compile or bind the contents of the current + * DOM element. This is useful if the element contains what appears to be Angular directives and + * bindings but which should be ignored by Angular. This could be the case if you have a site that + * displays snippets of code, for instance. + * + * @element ANY + * + * @example + * In this example there are two locations where a simple interpolation binding (`{{}}`) is present, + * but the one wrapped in `ngNonBindable` is left alone. + * + * @example + + +
      Normal: {{1 + 2}}
      +
      Ignored: {{1 + 2}}
      +
      + + it('should check ng-non-bindable', function() { + 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 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 `
      + + + it('should show correct pluralized string', function() { + var withoutOffset = element.all(by.css('ng-pluralize')).get(0); + var withOffset = element.all(by.css('ng-pluralize')).get(1); + var countInput = element(by.model('personCount')); + + expect(withoutOffset.getText()).toEqual('1 person is viewing.'); + expect(withOffset.getText()).toEqual('Igor is viewing.'); + + countInput.clear(); + countInput.sendKeys('0'); + + expect(withoutOffset.getText()).toEqual('Nobody is viewing.'); + expect(withOffset.getText()).toEqual('Nobody is viewing.'); + + countInput.clear(); + countInput.sendKeys('2'); + + expect(withoutOffset.getText()).toEqual('2 people are viewing.'); + expect(withOffset.getText()).toEqual('Igor and Misko are viewing.'); + + countInput.clear(); + countInput.sendKeys('3'); + + expect(withoutOffset.getText()).toEqual('3 people are viewing.'); + expect(withOffset.getText()).toEqual('Igor, Misko and one other person are viewing.'); + + countInput.clear(); + countInput.sendKeys('4'); + + expect(withoutOffset.getText()).toEqual('4 people are viewing.'); + expect(withOffset.getText()).toEqual('Igor, Misko and 2 other people are viewing.'); + }); + it('should show data-bound names', function() { + var withOffset = element.all(by.css('ng-pluralize')).get(1); + var personCount = element(by.model('personCount')); + var person1 = element(by.model('person1')); + var person2 = element(by.model('person2')); + personCount.clear(); + personCount.sendKeys('4'); + person1.clear(); + person1.sendKeys('Di'); + person2.clear(); + person2.sendKeys('Vojta'); + expect(withOffset.getText()).toEqual('Di, Vojta and 2 other people are viewing.'); + }); + + + */ +var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale, $interpolate, $log) { + var BRACE = /{}/g, + IS_WHEN = /^when(Minus)?(.+)$/; + + return { + link: function(scope, element, attr) { + var numberExp = attr.count, + whenExp = attr.$attr.when && element.attr(attr.$attr.when), // we have {{}} in attrs + offset = attr.offset || 0, + whens = scope.$eval(whenExp) || {}, + whensExpFns = {}, + startSymbol = $interpolate.startSymbol(), + endSymbol = $interpolate.endSymbol(), + braceReplacement = startSymbol + numberExp + '-' + offset + endSymbol, + watchRemover = angular.noop, + lastCount; + + forEach(attr, function(expression, attributeName) { + var tmpMatch = IS_WHEN.exec(attributeName); + if (tmpMatch) { + var whenKey = (tmpMatch[1] ? '-' : '') + lowercase(tmpMatch[2]); + whens[whenKey] = element.attr(attr.$attr[attributeName]); + } + }); + forEach(whens, function(expression, key) { + whensExpFns[key] = $interpolate(expression.replace(BRACE, braceReplacement)); + + }); + + scope.$watch(numberExp, function ngPluralizeWatchAction(newVal) { + var count = parseFloat(newVal); + var countIsNaN = isNumberNaN(count); + + if (!countIsNaN && !(count in whens)) { + // If an explicit number rule such as 1, 2, 3... is defined, just use it. + // Otherwise, check it against pluralization rules in $locale service. + count = $locale.pluralCat(count - offset); + } + + // If both `count` and `lastCount` are NaN, we don't need to re-register a watch. + // In JS `NaN !== NaN`, so we have to explicitly check. + if ((count !== lastCount) && !(countIsNaN && isNumberNaN(lastCount))) { + watchRemover(); + var whenExpFn = whensExpFns[count]; + if (isUndefined(whenExpFn)) { + if (newVal != null) { + $log.debug('ngPluralize: no rule defined for \'' + count + '\' in ' + whenExp); + } + watchRemover = noop; + updateElementText(); + } else { + watchRemover = scope.$watch(whenExpFn, updateElementText); + } + lastCount = count; + } + }); + + function updateElementText(newText) { + element.text(newText || ''); + } + } + }; +}]; + +/* exported ngRepeatDirective */ + +/** + * @ngdoc directive + * @name ngRepeat + * @multiElement + * + * @description + * The `ngRepeat` directive instantiates a template once per item from a collection. Each template + * instance gets its own scope, where the given loop variable is set to the current collection item, + * and `$index` is set to the item index or key. + * + * Special properties are exposed on the local scope of each template instance, including: + * + * | Variable | Type | Details | + * |-----------|-----------------|-----------------------------------------------------------------------------| + * | `$index` | {@type number} | iterator offset of the repeated element (0..length-1) | + * | `$first` | {@type boolean} | true if the repeated element is first in the iterator. | + * | `$middle` | {@type boolean} | true if the repeated element is between the first and last in the iterator. | + * | `$last` | {@type boolean} | true if the repeated element is last in the iterator. | + * | `$even` | {@type boolean} | true if the iterator position `$index` is even (otherwise false). | + * | `$odd` | {@type boolean} | true if the iterator position `$index` is odd (otherwise false). | + * + *
      + * Creating aliases for these properties is possible with {@link ng.directive:ngInit `ngInit`}. + * This may be useful when, for instance, nesting ngRepeats. + *
      + * + * + * # Iterating over object properties + * + * It is possible to get `ngRepeat` to iterate over the properties of an object using the following + * syntax: + * + * ```js + *
      ...
      + * ``` + * + * However, there are a few limitations compared to array iteration: + * + * - The JavaScript specification does not define the order of keys + * returned for an object, so Angular relies on the order returned by the browser + * when running `for key in myObj`. Browsers generally follow the strategy of providing + * keys in the order in which they were defined, although there are exceptions when keys are deleted + * and reinstated. See the + * [MDN page on `delete` for more info](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/delete#Cross-browser_notes). + * + * - `ngRepeat` will silently *ignore* object keys starting with `$`, because + * it's a prefix used by Angular for public (`$`) and private (`$$`) properties. + * + * - The built-in filters {@link ng.orderBy orderBy} and {@link ng.filter filter} do not work with + * objects, and will throw an error if used with one. + * + * If you are hitting any of these limitations, the recommended workaround is to convert your object into an array + * that is sorted into the order that you prefer before providing it to `ngRepeat`. You could + * do this with a filter such as [toArrayFilter](http://ngmodules.org/modules/angular-toArrayFilter) + * or implement a `$watch` on the object yourself. + * + * + * # Tracking and Duplicates + * + * `ngRepeat` uses {@link $rootScope.Scope#$watchCollection $watchCollection} to detect changes in + * the collection. When a change happens, `ngRepeat` then makes the corresponding changes to the DOM: + * + * * When an item is added, a new instance of the template is added to the DOM. + * * When an item is removed, its template instance is removed from the DOM. + * * When items are reordered, their respective templates are reordered in the DOM. + * + * To minimize creation of DOM elements, `ngRepeat` uses a function + * to "keep track" of all items in the collection and their corresponding DOM elements. + * For example, if an item is added to the collection, `ngRepeat` will know that all other items + * already have DOM elements, and will not re-render them. + * + * The default tracking function (which tracks items by their identity) does not allow + * duplicate items in arrays. This is because when there are duplicates, it is not possible + * to maintain a one-to-one mapping between collection items and DOM elements. + * + * If you do need to repeat duplicate items, you can substitute the default tracking behavior + * with your own using the `track by` expression. + * + * For example, you may track items by the index of each item in the collection, using the + * special scope property `$index`: + * ```html + *
      + * {{n}} + *
      + * ``` + * + * You may also use arbitrary expressions in `track by`, including references to custom functions + * on the scope: + * ```html + *
      + * {{n}} + *
      + * ``` + * + *
      + * If you are working with objects that have a unique identifier property, you should track + * by this identifier instead of the object instance. Should you reload your data later, `ngRepeat` + * will not have to rebuild the DOM elements for items it has already rendered, even if the + * JavaScript objects in the collection have been substituted for new ones. For large collections, + * this significantly improves rendering performance. If you don't have a unique identifier, + * `track by $index` can also provide a performance boost. + *
      + * + * ```html + *
      + * {{model.name}} + *
      + * ``` + * + *
      + *
      + * Avoid using `track by $index` when the repeated template contains + * {@link guide/expression#one-time-binding one-time bindings}. In such cases, the `nth` DOM + * element will always be matched with the `nth` item of the array, so the bindings on that element + * will not be updated even when the corresponding item changes, essentially causing the view to get + * out-of-sync with the underlying data. + *
      + * + * When no `track by` expression is provided, it is equivalent to tracking by the built-in + * `$id` function, which tracks items by their identity: + * ```html + *
      + * {{obj.prop}} + *
      + * ``` + * + *
      + *
      + * **Note:** `track by` must always be the last expression: + *
      + * ``` + *
      + * {{model.name}} + *
      + * ``` + * + * + * # Special repeat start and end points + * To repeat a series of elements instead of just one parent element, ngRepeat (as well as other ng directives) supports extending + * the range of the repeater by defining explicit start and end points by using **ng-repeat-start** and **ng-repeat-end** respectively. + * The **ng-repeat-start** directive works the same as **ng-repeat**, but will repeat all the HTML code (including the tag it's defined on) + * up to and including the ending HTML tag where **ng-repeat-end** is placed. + * + * The example below makes use of this feature: + * ```html + *
      + * Header {{ item }} + *
      + *
      + * Body {{ item }} + *
      + *
      + * Footer {{ item }} + *
      + * ``` + * + * And with an input of {@type ['A','B']} for the items variable in the example above, the output will evaluate to: + * ```html + *
      + * Header A + *
      + *
      + * Body A + *
      + *
      + * Footer A + *
      + *
      + * Header B + *
      + *
      + * Body B + *
      + *
      + * Footer B + *
      + * ``` + * + * The custom start and end points for ngRepeat also support all other HTML directive syntax flavors provided in AngularJS (such + * as **data-ng-repeat-start**, **x-ng-repeat-start** and **ng:repeat-start**). + * + * @animations + * | Animation | Occurs | + * |----------------------------------|-------------------------------------| + * | {@link ng.$animate#enter enter} | when a new item is added to the list or when an item is revealed after a filter | + * | {@link ng.$animate#leave leave} | when an item is removed from the list or when an item is filtered out | + * | {@link ng.$animate#move move } | when an adjacent item is filtered out causing a reorder or when the item contents are reordered | + * + * See the example below for defining CSS animations with ngRepeat. + * + * @element ANY + * @scope + * @priority 1000 + * @param {repeat_expression} ngRepeat The expression indicating how to enumerate a collection. These + * formats are currently supported: + * + * * `variable in expression` – where variable is the user defined loop variable and `expression` + * is a scope expression giving the collection to enumerate. + * + * For example: `album in artist.albums`. + * + * * `(key, value) in expression` – where `key` and `value` can be any user defined identifiers, + * and `expression` is the scope expression giving the collection to enumerate. + * + * For example: `(name, age) in {'adam':10, 'amalie':12}`. + * + * * `variable in expression track by tracking_expression` – You can also provide an optional tracking expression + * which can be used to associate the objects in the collection with the DOM elements. If no tracking expression + * is specified, ng-repeat associates elements by identity. It is an error to have + * more than one tracking expression value resolve to the same key. (This would mean that two distinct objects are + * mapped to the same DOM element, which is not possible.) + * + * Note that the tracking expression must come last, after any filters, and the alias expression. + * + * For example: `item in items` is equivalent to `item in items track by $id(item)`. This implies that the DOM elements + * will be associated by item identity in the array. + * + * For example: `item in items track by $id(item)`. A built in `$id()` function can be used to assign a unique + * `$$hashKey` property to each item in the array. This property is then used as a key to associated DOM elements + * with the corresponding item in the array by identity. Moving the same object in array would move the DOM + * element in the same way in the DOM. + * + * For example: `item in items track by item.id` is a typical pattern when the items come from the database. In this + * case the object identity does not matter. Two objects are considered equivalent as long as their `id` + * property is same. + * + * For example: `item in items | filter:searchText track by item.id` is a pattern that might be used to apply a filter + * to items in conjunction with a tracking expression. + * + * * `variable in expression as alias_expression` – You can also provide an optional alias expression which will then store the + * intermediate results of the repeater after the filters have been applied. Typically this is used to render a special message + * when a filter is active on the repeater, but the filtered result set is empty. + * + * For example: `item in items | filter:x as results` will store the fragment of the repeated items as `results`, but only after + * the items have been processed through the filter. + * + * Please note that `as [variable name] is not an operator but rather a part of ngRepeat micro-syntax so it can be used only at the end + * (and not as operator, inside an expression). + * + * For example: `item in items | filter : x | orderBy : order | limitTo : limit as results` . + * + * @example + * This example uses `ngRepeat` to display a list of people. A filter is used to restrict the displayed + * results by name or by age. New (entering) and removed (leaving) items are animated. + + +
      + I have {{friends.length}} friends. They are: + +
        +
      • + [{{$index + 1}}] {{friend.name}} who is {{friend.age}} years old. +
      • +
      • + No results found... +
      • +
      +
      +
      + + angular.module('ngRepeat', ['ngAnimate']).controller('repeatController', function($scope) { + $scope.friends = [ + {name:'John', age:25, gender:'boy'}, + {name:'Jessie', age:30, gender:'girl'}, + {name:'Johanna', age:28, gender:'girl'}, + {name:'Joy', age:15, gender:'girl'}, + {name:'Mary', age:28, gender:'girl'}, + {name:'Peter', age:95, gender:'boy'}, + {name:'Sebastian', age:50, gender:'boy'}, + {name:'Erika', age:27, gender:'girl'}, + {name:'Patrick', age:40, gender:'boy'}, + {name:'Samantha', age:60, gender:'girl'} + ]; + }); + + + .example-animate-container { + background:white; + border:1px solid black; + list-style:none; + margin:0; + padding:0 10px; + } + + .animate-repeat { + line-height:30px; + list-style:none; + box-sizing:border-box; + } + + .animate-repeat.ng-move, + .animate-repeat.ng-enter, + .animate-repeat.ng-leave { + transition:all linear 0.5s; + } + + .animate-repeat.ng-leave.ng-leave-active, + .animate-repeat.ng-move, + .animate-repeat.ng-enter { + opacity:0; + max-height:0; + } + + .animate-repeat.ng-leave, + .animate-repeat.ng-move.ng-move-active, + .animate-repeat.ng-enter.ng-enter-active { + opacity:1; + max-height:30px; + } + + + var friends = element.all(by.repeater('friend in friends')); + + it('should render initial data set', function() { + expect(friends.count()).toBe(10); + expect(friends.get(0).getText()).toEqual('[1] John who is 25 years old.'); + expect(friends.get(1).getText()).toEqual('[2] Jessie who is 30 years old.'); + expect(friends.last().getText()).toEqual('[10] Samantha who is 60 years old.'); + expect(element(by.binding('friends.length')).getText()) + .toMatch("I have 10 friends. They are:"); + }); + + it('should update repeater when filter predicate changes', function() { + expect(friends.count()).toBe(10); + + element(by.model('q')).sendKeys('ma'); + + expect(friends.count()).toBe(2); + expect(friends.get(0).getText()).toEqual('[1] Mary who is 28 years old.'); + expect(friends.last().getText()).toEqual('[2] Samantha who is 60 years old.'); + }); + +
      + */ +var ngRepeatDirective = ['$parse', '$animate', '$compile', function($parse, $animate, $compile) { + var NG_REMOVED = '$$NG_REMOVED'; + var ngRepeatMinErr = minErr('ngRepeat'); + + var updateScope = function(scope, index, valueIdentifier, value, keyIdentifier, key, arrayLength) { + // TODO(perf): generate setters to shave off ~40ms or 1-1.5% + scope[valueIdentifier] = value; + if (keyIdentifier) scope[keyIdentifier] = key; + scope.$index = index; + scope.$first = (index === 0); + scope.$last = (index === (arrayLength - 1)); + scope.$middle = !(scope.$first || scope.$last); + // eslint-disable-next-line no-bitwise + scope.$odd = !(scope.$even = (index & 1) === 0); + }; + + var getBlockStart = function(block) { + return block.clone[0]; + }; + + var getBlockEnd = function(block) { + return block.clone[block.clone.length - 1]; + }; + + + return { + restrict: 'A', + multiElement: true, + transclude: 'element', + priority: 1000, + terminal: true, + $$tlb: true, + compile: function ngRepeatCompile($element, $attr) { + var expression = $attr.ngRepeat; + var ngRepeatEndComment = $compile.$$createComment('end ngRepeat', expression); + + var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+track\s+by\s+([\s\S]+?))?\s*$/); + + if (!match) { + throw ngRepeatMinErr('iexp', 'Expected expression in form of \'_item_ in _collection_[ track by _id_]\' but got \'{0}\'.', + expression); + } + + var lhs = match[1]; + var rhs = match[2]; + var aliasAs = match[3]; + var trackByExp = match[4]; + + match = lhs.match(/^(?:(\s*[$\w]+)|\(\s*([$\w]+)\s*,\s*([$\w]+)\s*\))$/); + + if (!match) { + throw ngRepeatMinErr('iidexp', '\'_item_\' in \'_item_ in _collection_\' should be an identifier or \'(_key_, _value_)\' expression, but got \'{0}\'.', + lhs); + } + var valueIdentifier = match[3] || match[1]; + var keyIdentifier = match[2]; + + if (aliasAs && (!/^[$a-zA-Z_][$a-zA-Z0-9_]*$/.test(aliasAs) || + /^(null|undefined|this|\$index|\$first|\$middle|\$last|\$even|\$odd|\$parent|\$root|\$id)$/.test(aliasAs))) { + throw ngRepeatMinErr('badident', 'alias \'{0}\' is invalid --- must be a valid JS identifier which is not a reserved name.', + aliasAs); + } + + var trackByExpGetter, trackByIdExpFn, trackByIdArrayFn, trackByIdObjFn; + var hashFnLocals = {$id: hashKey}; + + if (trackByExp) { + trackByExpGetter = $parse(trackByExp); + } else { + trackByIdArrayFn = function(key, value) { + return hashKey(value); + }; + trackByIdObjFn = function(key) { + return key; + }; + } + + return function ngRepeatLink($scope, $element, $attr, ctrl, $transclude) { + + if (trackByExpGetter) { + trackByIdExpFn = function(key, value, index) { + // assign key, value, and $index to the locals so that they can be used in hash functions + if (keyIdentifier) hashFnLocals[keyIdentifier] = key; + hashFnLocals[valueIdentifier] = value; + hashFnLocals.$index = index; + return trackByExpGetter($scope, hashFnLocals); + }; + } + + // Store a list of elements from previous run. This is a hash where key is the item from the + // iterator, and the value is objects with following properties. + // - scope: bound scope + // - element: previous element. + // - index: position + // + // We are using no-proto object so that we don't need to guard against inherited props via + // hasOwnProperty. + var lastBlockMap = createMap(); + + //watch props + $scope.$watchCollection(rhs, function ngRepeatAction(collection) { + var index, length, + previousNode = $element[0], // node that cloned nodes should be inserted after + // initialized to the comment node anchor + nextNode, + // Same as lastBlockMap but it has the current state. It will become the + // lastBlockMap on the next iteration. + nextBlockMap = createMap(), + collectionLength, + key, value, // key/value of iteration + trackById, + trackByIdFn, + collectionKeys, + block, // last object information {scope, element, id} + nextBlockOrder, + elementsToRemove; + + if (aliasAs) { + $scope[aliasAs] = collection; + } + + if (isArrayLike(collection)) { + collectionKeys = collection; + trackByIdFn = trackByIdExpFn || trackByIdArrayFn; + } else { + trackByIdFn = trackByIdExpFn || trackByIdObjFn; + // if object, extract keys, in enumeration order, unsorted + collectionKeys = []; + for (var itemKey in collection) { + if (hasOwnProperty.call(collection, itemKey) && itemKey.charAt(0) !== '$') { + collectionKeys.push(itemKey); + } + } + } + + collectionLength = collectionKeys.length; + nextBlockOrder = new Array(collectionLength); + + // locate existing items + for (index = 0; index < collectionLength; index++) { + key = (collection === collectionKeys) ? index : collectionKeys[index]; + value = collection[key]; + trackById = trackByIdFn(key, value, index); + if (lastBlockMap[trackById]) { + // found previously seen block + block = lastBlockMap[trackById]; + delete lastBlockMap[trackById]; + nextBlockMap[trackById] = block; + nextBlockOrder[index] = block; + } else if (nextBlockMap[trackById]) { + // if collision detected. restore lastBlockMap and throw an error + forEach(nextBlockOrder, function(block) { + if (block && block.scope) lastBlockMap[block.id] = block; + }); + throw ngRepeatMinErr('dupes', + 'Duplicates in a repeater are not allowed. Use \'track by\' expression to specify unique keys. Repeater: {0}, Duplicate key: {1}, Duplicate value: {2}', + expression, trackById, value); + } else { + // new never before seen block + nextBlockOrder[index] = {id: trackById, scope: undefined, clone: undefined}; + nextBlockMap[trackById] = true; + } + } + + // remove leftover items + for (var blockKey in lastBlockMap) { + block = lastBlockMap[blockKey]; + elementsToRemove = getBlockNodes(block.clone); + $animate.leave(elementsToRemove); + if (elementsToRemove[0].parentNode) { + // if the element was not removed yet because of pending animation, mark it as deleted + // so that we can ignore it later + for (index = 0, length = elementsToRemove.length; index < length; index++) { + elementsToRemove[index][NG_REMOVED] = true; + } + } + block.scope.$destroy(); + } + + // we are not using forEach for perf reasons (trying to avoid #call) + for (index = 0; index < collectionLength; index++) { + key = (collection === collectionKeys) ? index : collectionKeys[index]; + value = collection[key]; + block = nextBlockOrder[index]; + + if (block.scope) { + // if we have already seen this object, then we need to reuse the + // associated scope/element + + nextNode = previousNode; + + // skip nodes that are already pending removal via leave animation + do { + nextNode = nextNode.nextSibling; + } while (nextNode && nextNode[NG_REMOVED]); + + if (getBlockStart(block) !== nextNode) { + // existing item which got moved + $animate.move(getBlockNodes(block.clone), null, previousNode); + } + previousNode = getBlockEnd(block); + updateScope(block.scope, index, valueIdentifier, value, keyIdentifier, key, collectionLength); + } else { + // new item which we don't know about + $transclude(function ngRepeatTransclude(clone, scope) { + block.scope = scope; + // http://jsperf.com/clone-vs-createcomment + var endNode = ngRepeatEndComment.cloneNode(false); + clone[clone.length++] = endNode; + + $animate.enter(clone, null, previousNode); + previousNode = endNode; + // 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 its template arrives. + block.clone = clone; + nextBlockMap[block.id] = block; + updateScope(block.scope, index, valueIdentifier, value, keyIdentifier, key, collectionLength); + }); + } + } + lastBlockMap = nextBlockMap; + }); + }; + } + }; +}]; + +var NG_HIDE_CLASS = 'ng-hide'; +var NG_HIDE_IN_PROGRESS_CLASS = 'ng-hide-animate'; +/** + * @ngdoc directive + * @name ngShow + * @multiElement + * + * @description + * The `ngShow` directive shows or hides the given HTML element based on the expression + * provided to the `ngShow` attribute. The element is shown or hidden by removing or adding + * the `.ng-hide` CSS class onto the element. The `.ng-hide` CSS class is predefined + * in AngularJS and sets the display style to none (using an !important flag). + * For CSP mode please add `angular-csp.css` to your html file (see {@link ng.directive:ngCsp ngCsp}). + * + * ```html + * + *
      + * + * + *
      + * ``` + * + * When the `ngShow` expression evaluates to a falsy value then the `.ng-hide` CSS class is added to the class + * attribute on the element causing it to become hidden. When truthy, the `.ng-hide` CSS class is removed + * from the element causing the element not to appear hidden. + * + * ## Why is !important used? + * + * You may be wondering why !important is used for the `.ng-hide` CSS class. This is because the `.ng-hide` selector + * can be easily overridden by heavier selectors. For example, something as simple + * as changing the display style on a HTML list item would make hidden elements appear visible. + * This also becomes a bigger issue when dealing with CSS frameworks. + * + * By using !important, the show and hide behavior will work as expected despite any clash between CSS selector + * specificity (when !important isn't used with any conflicting styles). If a developer chooses to override the + * styling to change how to hide an element then it is just a matter of using !important in their own CSS code. + * + * ### Overriding `.ng-hide` + * + * By default, the `.ng-hide` class will style the element with `display: none!important`. If you wish to change + * the hide behavior with ngShow/ngHide then this can be achieved by restating the styles for the `.ng-hide` + * class CSS. Note that the selector that needs to be used is actually `.ng-hide:not(.ng-hide-animate)` to cope + * with extra animation classes that can be added. + * + * ```css + * .ng-hide:not(.ng-hide-animate) { + * /* this is just another form of hiding an element */ + * display: block!important; + * position: absolute; + * top: -9999px; + * left: -9999px; + * } + * ``` + * + * By default you don't need to override in CSS anything and the animations will work around the display style. + * + * ## A note about animations with `ngShow` + * + * Animations in ngShow/ngHide work with the show and hide events that are triggered when the directive expression + * is true and false. This system works like the animation system present with ngClass except that + * you must also include the !important flag to override the display property + * so that you can perform an animation when the element is hidden during the time of the animation. + * + * ```css + * // + * //a working example can be found at the bottom of this page + * // + * .my-element.ng-hide-add, .my-element.ng-hide-remove { + * /* this is required as of 1.3x to properly + * apply all styling in a show/hide animation */ + * transition: 0s linear all; + * } + * + * .my-element.ng-hide-add-active, + * .my-element.ng-hide-remove-active { + * /* the transition is defined in the active class */ + * transition: 1s linear all; + * } + * + * .my-element.ng-hide-add { ... } + * .my-element.ng-hide-add.ng-hide-add-active { ... } + * .my-element.ng-hide-remove { ... } + * .my-element.ng-hide-remove.ng-hide-remove-active { ... } + * ``` + * + * Keep in mind that, as of AngularJS version 1.3, there is no need to change the display + * property to block during animation states--ngAnimate will handle the style toggling automatically for you. + * + * @animations + * | Animation | Occurs | + * |----------------------------------|-------------------------------------| + * | {@link $animate#addClass addClass} `.ng-hide` | after the `ngShow` expression evaluates to a non truthy value and just before the contents are set to hidden | + * | {@link $animate#removeClass removeClass} `.ng-hide` | after the `ngShow` expression evaluates to a truthy value and just before contents are set to visible | + * + * @element ANY + * @param {expression} ngShow If the {@link guide/expression expression} is truthy + * then the element is shown or hidden respectively. + * + * @example + + + Click me:
      +
      + Show: +
      + I show up when your checkbox is checked. +
      +
      +
      + Hide: +
      + I hide when your checkbox is checked. +
      +
      +
      + + @import url(../../components/bootstrap-3.1.1/css/bootstrap.css); + + + .animate-show { + line-height: 20px; + opacity: 1; + padding: 10px; + border: 1px solid black; + background: white; + } + + .animate-show.ng-hide-add, .animate-show.ng-hide-remove { + transition: all linear 0.5s; + } + + .animate-show.ng-hide { + line-height: 0; + opacity: 0; + padding: 0 10px; + } + + .check-element { + padding: 10px; + border: 1px solid black; + background: white; + } + + + var thumbsUp = element(by.css('span.glyphicon-thumbs-up')); + var thumbsDown = element(by.css('span.glyphicon-thumbs-down')); + + it('should check ng-show / ng-hide', function() { + expect(thumbsUp.isDisplayed()).toBeFalsy(); + expect(thumbsDown.isDisplayed()).toBeTruthy(); + + element(by.model('checked')).click(); + + expect(thumbsUp.isDisplayed()).toBeTruthy(); + expect(thumbsDown.isDisplayed()).toBeFalsy(); + }); + +
      + */ +var ngShowDirective = ['$animate', function($animate) { + return { + restrict: 'A', + multiElement: true, + link: function(scope, element, attr) { + scope.$watch(attr.ngShow, function ngShowWatchAction(value) { + // we're adding a temporary, animation-specific class for ng-hide since this way + // we can control when the element is actually displayed on screen without having + // to have a global/greedy CSS selector that breaks when other animations are run. + // Read: https://github.com/angular/angular.js/issues/9103#issuecomment-58335845 + $animate[value ? 'removeClass' : 'addClass'](element, NG_HIDE_CLASS, { + tempClasses: NG_HIDE_IN_PROGRESS_CLASS + }); + }); + } + }; +}]; + + +/** + * @ngdoc directive + * @name ngHide + * @multiElement + * + * @description + * The `ngHide` directive shows or hides the given HTML element based on the expression + * provided to the `ngHide` attribute. The element is shown or hidden by removing or adding + * the `ng-hide` CSS class onto the element. The `.ng-hide` CSS class is predefined + * in AngularJS and sets the display style to none (using an !important flag). + * For CSP mode please add `angular-csp.css` to your html file (see {@link ng.directive:ngCsp ngCsp}). + * + * ```html + * + *
      + * + * + *
      + * ``` + * + * When the `ngHide` expression evaluates to a truthy value then the `.ng-hide` CSS class is added to the class + * attribute on the element causing it to become hidden. When falsy, the `.ng-hide` CSS class is removed + * from the element causing the element not to appear hidden. + * + * ## Why is !important used? + * + * You may be wondering why !important is used for the `.ng-hide` CSS class. This is because the `.ng-hide` selector + * can be easily overridden by heavier selectors. For example, something as simple + * as changing the display style on a HTML list item would make hidden elements appear visible. + * This also becomes a bigger issue when dealing with CSS frameworks. + * + * By using !important, the show and hide behavior will work as expected despite any clash between CSS selector + * specificity (when !important isn't used with any conflicting styles). If a developer chooses to override the + * styling to change how to hide an element then it is just a matter of using !important in their own CSS code. + * + * ### Overriding `.ng-hide` + * + * By default, the `.ng-hide` class will style the element with `display: none!important`. If you wish to change + * the hide behavior with ngShow/ngHide then this can be achieved by restating the styles for the `.ng-hide` + * class in CSS: + * + * ```css + * .ng-hide { + * /* this is just another form of hiding an element */ + * display: block!important; + * position: absolute; + * top: -9999px; + * left: -9999px; + * } + * ``` + * + * By default you don't need to override in CSS anything and the animations will work around the display style. + * + * ## A note about animations with `ngHide` + * + * Animations in ngShow/ngHide work with the show and hide events that are triggered when the directive expression + * is true and false. This system works like the animation system present with ngClass, except that the `.ng-hide` + * CSS class is added and removed for you instead of your own CSS class. + * + * ```css + * // + * //a working example can be found at the bottom of this page + * // + * .my-element.ng-hide-add, .my-element.ng-hide-remove { + * transition: 0.5s linear all; + * } + * + * .my-element.ng-hide-add { ... } + * .my-element.ng-hide-add.ng-hide-add-active { ... } + * .my-element.ng-hide-remove { ... } + * .my-element.ng-hide-remove.ng-hide-remove-active { ... } + * ``` + * + * Keep in mind that, as of AngularJS version 1.3, there is no need to change the display + * property to block during animation states--ngAnimate will handle the style toggling automatically for you. + * + * @animations + * | Animation | Occurs | + * |----------------------------------|-------------------------------------| + * | {@link $animate#addClass addClass} `.ng-hide` | after the `ngHide` expression evaluates to a truthy value and just before the contents are set to hidden | + * | {@link $animate#removeClass removeClass} `.ng-hide` | after the `ngHide` expression evaluates to a non truthy value and just before contents are set to visible | + * + * + * @element ANY + * @param {expression} ngHide If the {@link guide/expression expression} is truthy then + * the element is shown or hidden respectively. + * + * @example + + + Click me:
      +
      + Show: +
      + I show up when your checkbox is checked. +
      +
      +
      + Hide: +
      + I hide when your checkbox is checked. +
      +
      +
      + + @import url(../../components/bootstrap-3.1.1/css/bootstrap.css); + + + .animate-hide { + transition: all linear 0.5s; + line-height: 20px; + opacity: 1; + padding: 10px; + border: 1px solid black; + background: white; + } + + .animate-hide.ng-hide { + line-height: 0; + opacity: 0; + padding: 0 10px; + } + + .check-element { + padding: 10px; + border: 1px solid black; + background: white; + } + + + var thumbsUp = element(by.css('span.glyphicon-thumbs-up')); + var thumbsDown = element(by.css('span.glyphicon-thumbs-down')); + + it('should check ng-show / ng-hide', function() { + expect(thumbsUp.isDisplayed()).toBeFalsy(); + expect(thumbsDown.isDisplayed()).toBeTruthy(); + + element(by.model('checked')).click(); + + expect(thumbsUp.isDisplayed()).toBeTruthy(); + expect(thumbsDown.isDisplayed()).toBeFalsy(); + }); + +
      + */ +var ngHideDirective = ['$animate', function($animate) { + return { + restrict: 'A', + multiElement: true, + link: function(scope, element, attr) { + scope.$watch(attr.ngHide, function ngHideWatchAction(value) { + // The comment inside of the ngShowDirective explains why we add and + // remove a temporary class for the show/hide animation + $animate[value ? 'addClass' : 'removeClass'](element,NG_HIDE_CLASS, { + tempClasses: NG_HIDE_IN_PROGRESS_CLASS + }); + }); + } + }; +}]; + +/** + * @ngdoc directive + * @name ngStyle + * @restrict AC + * + * @description + * The `ngStyle` directive allows you to set CSS style on an HTML element conditionally. + * + * @knownIssue + * You should not use {@link guide/interpolation interpolation} in the value of the `style` + * attribute, when using the `ngStyle` directive on the same element. + * See {@link guide/interpolation#known-issues here} for more info. + * + * @element ANY + * @param {expression} ngStyle + * + * {@link guide/expression Expression} which evals to an + * object whose keys are CSS style names and values are corresponding values for those CSS + * keys. + * + * Since some CSS style names are not valid keys for an object, they must be quoted. + * See the 'background-color' style in the example below. + * + * @example + + + + + +
      + Sample Text +
      myStyle={{myStyle}}
      +
      + + span { + color: black; + } + + + var colorSpan = element(by.css('span')); + + it('should check ng-style', function() { + expect(colorSpan.getCssValue('color')).toBe('rgba(0, 0, 0, 1)'); + element(by.css('input[value=\'set color\']')).click(); + expect(colorSpan.getCssValue('color')).toBe('rgba(255, 0, 0, 1)'); + element(by.css('input[value=clear]')).click(); + expect(colorSpan.getCssValue('color')).toBe('rgba(0, 0, 0, 1)'); + }); + +
      + */ +var ngStyleDirective = ngDirective(function(scope, element, attr) { + scope.$watch(attr.ngStyle, function ngStyleWatchAction(newStyles, oldStyles) { + if (oldStyles && (newStyles !== oldStyles)) { + forEach(oldStyles, function(val, style) { element.css(style, '');}); + } + if (newStyles) element.css(newStyles); + }, true); +}); + +/** + * @ngdoc directive + * @name ngSwitch + * @restrict EA + * + * @description + * The `ngSwitch` directive is used to conditionally swap DOM structure on your template based on a scope expression. + * Elements within `ngSwitch` but without `ngSwitchWhen` or `ngSwitchDefault` directives will be preserved at the location + * as specified in the template. + * + * The directive itself works similar to ngInclude, however, instead of downloading template code (or loading it + * from the template cache), `ngSwitch` simply chooses one of the nested elements and makes it visible based on which element + * matches the value obtained from the evaluated expression. In other words, you define a container element + * (where you place the directive), place an expression on the **`on="..."` attribute** + * (or the **`ng-switch="..."` attribute**), define any inner elements inside of the directive and place + * a when attribute per element. The when attribute is used to inform ngSwitch which element to display when the on + * expression is evaluated. If a matching expression is not found via a when attribute then an element with the default + * attribute is displayed. + * + *
      + * Be aware that the attribute values to match against cannot be expressions. They are interpreted + * as literal string values to match against. + * For example, **`ng-switch-when="someVal"`** will match against the string `"someVal"` not against the + * value of the expression `$scope.someVal`. + *
      + + * @animations + * | Animation | Occurs | + * |----------------------------------|-------------------------------------| + * | {@link ng.$animate#enter enter} | after the ngSwitch contents change and the matched child element is placed inside the container | + * | {@link ng.$animate#leave leave} | after the ngSwitch contents change and just before the former contents are removed from the DOM | + * + * @usage + * + * ``` + * + * ... + * ... + * ... + * + * ``` + * + * + * @scope + * @priority 1200 + * @param {*} ngSwitch|on expression to match against ng-switch-when. + * On child elements add: + * + * * `ngSwitchWhen`: the case statement to match against. If match then this + * case will be displayed. If the same match appears multiple times, all the + * elements will be displayed. It is possible to associate multiple values to + * the same `ngSwitchWhen` by defining the optional attribute + * `ngSwitchWhenSeparator`. The separator will be used to split the value of + * the `ngSwitchWhen` attribute into multiple tokens, and the element will show + * if any of the `ngSwitch` evaluates to any of these tokens. + * * `ngSwitchDefault`: the default case when no other case match. If there + * are multiple default cases, all of them will be displayed when no other + * case match. + * + * + * @example + + +
      + + selection={{selection}} +
      +
      +
      Settings Div
      +
      Home Span
      +
      default
      +
      +
      +
      + + angular.module('switchExample', ['ngAnimate']) + .controller('ExampleController', ['$scope', function($scope) { + $scope.items = ['settings', 'home', 'options', 'other']; + $scope.selection = $scope.items[0]; + }]); + + + .animate-switch-container { + position:relative; + background:white; + border:1px solid black; + height:40px; + overflow:hidden; + } + + .animate-switch { + padding:10px; + } + + .animate-switch.ng-animate { + transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; + + position:absolute; + top:0; + left:0; + right:0; + bottom:0; + } + + .animate-switch.ng-leave.ng-leave-active, + .animate-switch.ng-enter { + top:-50px; + } + .animate-switch.ng-leave, + .animate-switch.ng-enter.ng-enter-active { + top:0; + } + + + var switchElem = element(by.css('[ng-switch]')); + var select = element(by.model('selection')); + + it('should start in settings', function() { + expect(switchElem.getText()).toMatch(/Settings Div/); + }); + it('should change to home', function() { + select.all(by.css('option')).get(1).click(); + expect(switchElem.getText()).toMatch(/Home Span/); + }); + it('should change to settings via "options"', function() { + select.all(by.css('option')).get(2).click(); + expect(switchElem.getText()).toMatch(/Settings Div/); + }); + it('should select default', function() { + select.all(by.css('option')).get(3).click(); + expect(switchElem.getText()).toMatch(/default/); + }); + +
      + */ +var ngSwitchDirective = ['$animate', '$compile', function($animate, $compile) { + return { + require: 'ngSwitch', + + // asks for $scope to fool the BC controller module + controller: ['$scope', function NgSwitchController() { + this.cases = {}; + }], + link: function(scope, element, attr, ngSwitchController) { + var watchExpr = attr.ngSwitch || attr.on, + selectedTranscludes = [], + selectedElements = [], + previousLeaveAnimations = [], + selectedScopes = []; + + var spliceFactory = function(array, index) { + return function(response) { + if (response !== false) array.splice(index, 1); + }; + }; + + scope.$watch(watchExpr, function ngSwitchWatchAction(value) { + var i, ii; + + // Start with the last, in case the array is modified during the loop + while (previousLeaveAnimations.length) { + $animate.cancel(previousLeaveAnimations.pop()); + } + + for (i = 0, ii = selectedScopes.length; i < ii; ++i) { + var selected = getBlockNodes(selectedElements[i].clone); + selectedScopes[i].$destroy(); + var runner = previousLeaveAnimations[i] = $animate.leave(selected); + runner.done(spliceFactory(previousLeaveAnimations, i)); + } + + selectedElements.length = 0; + selectedScopes.length = 0; + + if ((selectedTranscludes = ngSwitchController.cases['!' + value] || ngSwitchController.cases['?'])) { + forEach(selectedTranscludes, function(selectedTransclude) { + selectedTransclude.transclude(function(caseElement, selectedScope) { + selectedScopes.push(selectedScope); + var anchor = selectedTransclude.element; + caseElement[caseElement.length++] = $compile.$$createComment('end ngSwitchWhen'); + var block = { clone: caseElement }; + + selectedElements.push(block); + $animate.enter(caseElement, anchor.parent(), anchor); + }); + }); + } + }); + } + }; +}]; + +var ngSwitchWhenDirective = ngDirective({ + transclude: 'element', + priority: 1200, + require: '^ngSwitch', + multiElement: true, + link: function(scope, element, attrs, ctrl, $transclude) { + + var cases = attrs.ngSwitchWhen.split(attrs.ngSwitchWhenSeparator).sort().filter( + // Filter duplicate cases + function(element, index, array) { return array[index - 1] !== element; } + ); + + forEach(cases, function(whenCase) { + ctrl.cases['!' + whenCase] = (ctrl.cases['!' + whenCase] || []); + ctrl.cases['!' + whenCase].push({ transclude: $transclude, element: element }); + }); + } +}); + +var ngSwitchDefaultDirective = ngDirective({ + transclude: 'element', + priority: 1200, + require: '^ngSwitch', + multiElement: true, + link: function(scope, element, attr, ctrl, $transclude) { + ctrl.cases['?'] = (ctrl.cases['?'] || []); + ctrl.cases['?'].push({ transclude: $transclude, element: element }); + } +}); + +/** + * @ngdoc directive + * @name ngTransclude + * @restrict EAC + * + * @description + * Directive that marks the insertion point for the transcluded DOM of the nearest parent directive that uses transclusion. + * + * You can specify that you want to insert a named transclusion slot, instead of the default slot, by providing the slot name + * as the value of the `ng-transclude` or `ng-transclude-slot` attribute. + * + * If the transcluded content is not empty (i.e. contains one or more DOM nodes, including whitespace text nodes), any existing + * content of this element will be removed before the transcluded content is inserted. + * If the transcluded content is empty, the existing content is left intact. This lets you provide fallback content in the case + * that no transcluded content is provided. + * + * @element ANY + * + * @param {string} ngTransclude|ngTranscludeSlot the name of the slot to insert at this point. If this is not provided, is empty + * or its value is the same as the name of the attribute then the default slot is used. + * + * @example + * ### Basic transclusion + * This example demonstrates basic transclusion of content into a component directive. + * + * + * + *
      + *
      + *
      + * {{text}} + *
      + *
      + * + * it('should have transcluded', function() { + * var titleElement = element(by.model('title')); + * titleElement.clear(); + * titleElement.sendKeys('TITLE'); + * var textElement = element(by.model('text')); + * textElement.clear(); + * textElement.sendKeys('TEXT'); + * expect(element(by.binding('title')).getText()).toEqual('TITLE'); + * expect(element(by.binding('text')).getText()).toEqual('TEXT'); + * }); + * + *
      + * + * @example + * ### Transclude fallback content + * This example shows how to use `NgTransclude` with fallback content, that + * is displayed if no transcluded content is provided. + * + * + * + * + * + * + * + * + * Button2 + * + * + * + * it('should have different transclude element content', function() { + * expect(element(by.id('fallback')).getText()).toBe('Button1'); + * expect(element(by.id('modified')).getText()).toBe('Button2'); + * }); + * + * + * + * @example + * ### Multi-slot transclusion + * This example demonstrates using multi-slot transclusion in a component directive. + * + * + * + *
      + *
      + *
      + * + * {{title}} + *

      {{text}}

      + *
      + *
      + *
      + * + * angular.module('multiSlotTranscludeExample', []) + * .directive('pane', function() { + * return { + * restrict: 'E', + * transclude: { + * 'title': '?paneTitle', + * 'body': 'paneBody', + * 'footer': '?paneFooter' + * }, + * template: '
      ' + + * '
      Fallback Title
      ' + + * '
      ' + + * '' + + * '
      ' + * }; + * }) + * .controller('ExampleController', ['$scope', function($scope) { + * $scope.title = 'Lorem Ipsum'; + * $scope.link = 'https://google.com'; + * $scope.text = 'Neque porro quisquam est qui dolorem ipsum quia dolor...'; + * }]); + *
      + * + * it('should have transcluded the title and the body', function() { + * var titleElement = element(by.model('title')); + * titleElement.clear(); + * titleElement.sendKeys('TITLE'); + * var textElement = element(by.model('text')); + * textElement.clear(); + * textElement.sendKeys('TEXT'); + * expect(element(by.css('.title')).getText()).toEqual('TITLE'); + * expect(element(by.binding('text')).getText()).toEqual('TEXT'); + * expect(element(by.css('.footer')).getText()).toEqual('Fallback Footer'); + * }); + * + *
      + */ +var ngTranscludeMinErr = minErr('ngTransclude'); +var ngTranscludeDirective = ['$compile', function($compile) { + return { + restrict: 'EAC', + terminal: true, + compile: function ngTranscludeCompile(tElement) { + + // Remove and cache any original content to act as a fallback + var fallbackLinkFn = $compile(tElement.contents()); + tElement.empty(); + + return function ngTranscludePostLink($scope, $element, $attrs, controller, $transclude) { + + if (!$transclude) { + throw ngTranscludeMinErr('orphan', + 'Illegal use of ngTransclude directive in the template! ' + + 'No parent directive that requires a transclusion found. ' + + 'Element: {0}', + startingTag($element)); + } + + + // If the attribute is of the form: `ng-transclude="ng-transclude"` then treat it like the default + if ($attrs.ngTransclude === $attrs.$attr.ngTransclude) { + $attrs.ngTransclude = ''; + } + var slotName = $attrs.ngTransclude || $attrs.ngTranscludeSlot; + + // If the slot is required and no transclusion content is provided then this call will throw an error + $transclude(ngTranscludeCloneAttachFn, null, slotName); + + // If the slot is optional and no transclusion content is provided then use the fallback content + if (slotName && !$transclude.isSlotFilled(slotName)) { + useFallbackContent(); + } + + function ngTranscludeCloneAttachFn(clone, transcludedScope) { + if (clone.length) { + $element.append(clone); + } else { + useFallbackContent(); + // There is nothing linked against the transcluded scope since no content was available, + // so it should be safe to clean up the generated scope. + transcludedScope.$destroy(); + } + } + + function useFallbackContent() { + // Since this is the fallback content rather than the transcluded content, + // we link against the scope of this directive rather than the transcluded scope + fallbackLinkFn($scope, function(clone) { + $element.append(clone); + }); + } + }; + } + }; +}]; + +/** + * @ngdoc directive + * @name script + * @restrict E + * + * @description + * Load the content of a ` + + Load inlined template +
      + + + it('should load template defined inside script tag', function() { + element(by.css('#tpl-link')).click(); + expect(element(by.css('#tpl-content')).getText()).toMatch(/Content of the template/); + }); + + + */ +var scriptDirective = ['$templateCache', function($templateCache) { + return { + restrict: 'E', + terminal: true, + compile: function(element, attr) { + if (attr.type === 'text/ng-template') { + var templateUrl = attr.id, + text = element[0].text; + + $templateCache.put(templateUrl, text); + } + } + }; +}]; + +/* exported selectDirective, optionDirective */ + +var noopNgModelController = { $setViewValue: noop, $render: noop }; + +function chromeHack(optionElement) { + // Workaround for https://code.google.com/p/chromium/issues/detail?id=381459 + // Adding an

    ;M-h`k z;?Wgh;u82?BJr4K3}0Waw|5uXJvc!0nHW5w5`zC z-9iTH>4wnJG28(V`$Z_Wvc>qtL~zaOAB-+MaZ%f2v((AAlX1Adb2}NWZeRPBvi;1= z>Uf{?E8yZ+z|9o;PudQ(=6w-5m0H}ae=?6us?RXbwDWnGf0lQDGkIBBPdqr7)Ba`2 zd5#TIvGEFfbU~eM*Aq}IWn(f`v$ah^+1zyfWw2J^mZ!RbqANM9PDQVen56?FwbE3R zqPQiFxzg>HhgQWojz@O@xcn51E<%Hu%gx{~!@iva7W79}X8?BCDqPjgx*5-61 zxuM%LoU7||RLF$rUzx!u?gmF4RK|@sdw|pTIUK+o^6qD?o2kqpw9pz1r3JMyHvs}- z$^Hxd$(4~h-znWk6VXpqyPYa}ukjfT6+7T8j9YBHM;6>be@RHSCEF`&H}@MgRLIYi zSlJ3XO$_UZP^^yP*}!pO$VhhCEFQe|6R{a>+`TDX1hYG*67ir+J|GG>4X{9RId>Ic z!RlL#j}alCNF~vS=3!eL*2$huN`U6aK5M)ID%B1R&ee=WWLl$hb9W=n4u^)Ax|NH5rEy+Jj79g%bz4JZh!~4 zWZU8wE0=RTNm8ujRr!i^29{J6yXR99)%%5MX>~ES52UzUmzeLrHyNAeHHKxvULtZs z+9P`9GFHe}&R$R;B8B)`}s$g3~wo+|l zTsTH(V|#!vcYuf?wGb+tuX6S>ktvX8D%hbJa#TG8UNsAL7 ze9V>E*b^vER3?^Lf|DEsPcO;B31J?ngRR`TxO73#FyUUz#O;C@x^#toPx?#1qW=K6 zVYWO*ZVR~Oi^$zv_&P$?xo15LH50{x-t1L@cwuMG+ z<(mEbirHCsn_mRa8<&olf6V?O^zLKj;(i^@N!$MbCMFnXHN6u4)5?Aj>ajoLFR9KS z61w+u^(v`krx8alASl20Mzw}Bozv5fT!xek+C<(3e7!Y0tG=~m+MLT1-D^^TiBPy-SfS0q- zmr7^D+*aGpPA)Z|`by`MC%JYMVf(py8uN%>FdWwYrQL|7I;@ma%vgZ;-?$1nIe1A# z#g_{xGE-~ZGS3~$0K?QWwbuK&x4ncjSbB*X$Z9e+*D{c3VH^Up3BXe9;utUHS#K;c z*cwjdi|728Mv=cWoHt$1&LwJiBfQLWn6Ac=uaox*)fWC{6dkdQ~ zOV~Mr11EYF>M=H1ZDTMPE>zAUh-AUh4JRjj5UY;i4^CLi#c1^?ww@xx%Z{@s&rC6o zG0n=zm%JK_tVdKPYx6Qb30jQc`GuBTYtEyCWwJcah#=}(;5ay$-CPRXbA@XHo}uqv zP~jV9FOvQ@*IN~7?I{gC5s zqtv)?xxQiv4c`1qTqYSqsMvD<0G}}ePWs|Cv)CEmL4WQVFl0nzs6%xgLC?f-!ndI#8lU&X1pJ% zL)Nhde9cg2{Pid`bXO9$1o0Jyfq+Gm8iF|{Ul0xBiqIcGsh6g{r|!(p zrS}SkRw>IUgDl2|;bCrF>EjbKp7F{y9JosRRJ$C0W?5m--!ZiQC*c;>YY-jGBHdw! z_>S*~3~p=D66=*jz?Eetsgzjkp=M2oWg#)kxplKOK4t_BQ?oDYsJ{X}B^lTwpL3|BP}qzAcT#kxI9OtxLz zqbpmDUvhOX2}zer5(OsEmw6Ob{{RVTaE2JIM41|6UtxLTWU7JY6BElYl(bxq2};vt z9m<}x!v<2YUO1LALCg?197T;(buFVS&C8X%IGJ4m$1@E!>R80nq>VTTe%(U|rnIF+bF)Q_RK; z4LkD*{^FzAnt-0pVN2;bjN5(BQtcBPUSW382Va;0T5yCk0k@QhktYgnSbExy;i_*w zdYBNQ+%#SW>F9|VV1DAPWe|a+qA1%id)y(7<660Pk&UEVK;ofW{-$Lw5~xZm%w|(b zZ!uy-l!m4_*E2fK;jHdD<`XBfSpNWk0mk&XId+c4~BUJnz%7zY{5bhN-nL1-HxmEIvIxHob07m7 z#%443%1(b|Q)~Aq0rdohu3SuL{{ZX-&7k67wVk5_L$4DswCYnRc{`c%P56S6xqFnF zW5idPce<3Arn<<(ko$pxCags82aiyex#9)r(UuEZD=`OAaz)%W3Rcr-IJ&MX4_3MA z0avV*EU|KKJTl&5v$pYEOQPvZ*7Kv8ME%y` zLN>seDr}c3WqOFJ7)Dx$b2E2XPN4*Cd`6QT+^W?K#pR7gvq-m=8`nN#La*F(#}ERr z;YL-IsNhW7h(w~zN@b-xXHyk)^KpT)YB9b405O>%?0SWs9ml`Wn5#1NFOAnT5nhq0 zY?_y&!^{%Zv$fjBSDceOrt|F;b7`s1<1nLLTxKaRV=BI`Xi}Op72EN|UHk{K4dZJ8 z8CB*fGgFApU|hz@C;ULhZ2OF6Tv`=>-~pLhgFUg=5FU3q4p?a5)8Ytc?kxO;DHXaZ zT0rQCJe|2AMXK{KEHWOar^M<}imK)=Q@kTUvdbbRXo7Zxa)>Q|`qgtl^G$@LGL zWMEWSr(n5D%O3&i6?1l&!de&%1ZK%YLT!x!gUl^01?O;sc`@czVAb&oUh+TyUmhx3 z$doOm&9lieo*(W}tA9y$Ht{r{EbK8&f98~7KGkQYW2@hU{yzzq@2u@_tgFX;CkQpH zAXSGQF)O6{5|0ivfq%r8^Lik_gSj4L2O%G8PT=1DxuvM=PyO%@xxGYyXm&<(A#n%CC@e5DO%vVQlqRFw` zs7iz(P7|4`a@FQG3)5!>oT0Nsr3Q@*rUp1i8lkH|iuITZHFOaeQ>O>0A4P2Xi>Y<{ zmIn_cYMg9<`bD&WMJ4zN)YYD)$y`*nR782PEN^S`E62p<;w&AZHcGl@A}`GU0AgP) zS25aFKs9mc0LwfKCnp>cF;RgfJh0W@#MN+d0lDW9i1a!q#5mOIo}S~YUV7-3_u>Pa z&oby|I`3qEL|Q0m}uK!6~NYG;=jpc*Hmq^(&Cu#9Kgy z!g%u-+_Jx!O{)=BUO0)q$jv@sW*)GYwaK{jxj3CnTpiF7fEL3PgNb=DhS+IiZB~(- zUgnqTVl3q*Oa&K(g{bmR-Up^ga94hzGD9o)K@?{Tv@zO+K4DlFeXblzDvJU#MXjlR z2~YyGI+*66W(>|72gIm?CL7GQ++6nxR7JlKtqX)x{{X1f-9&&zZnDs4nM2V^y`r>9 zlJxqRea1o5bkwflcL3(}gy#G;2by;jT)$~%-=bGP2z1Y=q17P@*kBoN4+&XgLTIr7 zMwIxBR{Gu&&sQ*zvR)(O_Zrqd;jm11o#q;~I)!1$`ILRpv}W3PgEMD7qf1u)CI0|| z;LZ*u6j>Fb0jop6iCKV)UZ|p|KH)ERD}rJ!_`fi^ zio{@QY|P}^4rXwovbPl5fWB5e3U|u|!PA+XT>j-+G3pZbwgP%neGze6>R~t? zd7Y~hq}LP7vx%P^?pFG*@VoO6fK;@Nz9DfpG~!+frtt~Z_?YhRx`*dqPy${SU=79q zMRdVt?cQ?>XI#31-MtK4FUu~FvauK*i;hdwfzIJo^87+y zP{bjzAYLs2C^$Sz(Ol}_PZ+}!)Wrqx@4J_qQlSkr=DhoaWi%XwuB@#F4yET9U=t7vBZDyvs+^x)O68MVBQXIlEF?uj zLc%zzL8o&eqmz>WMN(%PB0{~psbu7>_#i6sHFEk{yALwXG5AU`1MFvMfYZ}0h3%W> z1eoM4YXfQHK{bRfSUvfKCDw-7pTkgdm5Mz#8lqM^ZdgKkIs0`tv%uk=nRHyz2iY^9 zM9w_~vGQIgE*g_mz}~rzODD!*BL4uPF5QH?2t3#$3d6Iqdx*W!7jReZDuOF=@k57( zG%fQO%AYA~>ESuC`$c}}rHL#7?E~%U9v}Ueq_c{dag(Y2_XQ>6reRGeIT+hsmLO>? zbq$z|UV>J@&nD_xiJVbV*Bo%el{nEUaivP`8zu$u-xFZ#Biuy8V|NKfne#1+zkcOa zOkLS3>({8sGd@s8!^*_TbG&l_0g&Aq97O;Gz(XS0&A{r8qpVbRu0!q&ZDFqxgg+^g znt0%ZS0$*Jw*%@~T?Rnn1!Ic)OD1WY_Yl(SsMK$G>gSTzVJiS_w;714(LivP6@}a^ zS1?>HZ-a9c3&iG>a!h-1>T@*8GUtfmVDrZdLJxYvuI_Ok=rzdvoe@n{TPc*Kh@l>Be4Im$Gqi$C0GX%b+fQEKTDJiX~p2 z(6XsyoULjUgLLwFfXL;`?jq%Sxj}bUJR;63l}4wYRt+~*UZV#nSYZ@ZfY43N0h^g{ za{HIi<;m2vIO|Y6)wpq%`RxKIZ+*dR;vr@x_;O0Ma2B|zZtR_9EL(eB;#@#&i16X>oZ11?t8gnwDU0|)$6@j;T#JT*(cB`s>CC!-c&vPsVj-co>d7^t?VJeqt zS`AE4$TE-y*3_agom@e+lGwzcJ2cGIvgRPhuBqUP-7@EkmsOhLTXd}MWzLCh^#hjq zJwUCO2W&%uPY)5@3>)Tbb-ytk$l!J{yxUHDl?^zfKQmyrU7#63y1#CGV$5{4dty7CwoOAr!wQoDt7VvbxM+CXzcgsRpGR0rrl-<$PPI~ zCCT`bi^!|fD$vq$LTgAcw$ymDygC#m1UE-KjwLor^@b@XhnSe5w`%HL4VqSfmPN1I zD8t2vs6sQW*7Gk8jm&j&4Sdol$n0mgG1aU=;@lGHvmDWu>up8KNtqKEU1mUfnKV$D4NAo1*hVoZJ#|{y-QT$1!JbX z$FjF_{%{KL!DTacO3N(E_2#Y}HcakB%wiTAYmDEti{P6|OQS$d zR|kn@hl2W<3}n(mrDX4z?+lIKaHVO@JW8rww8g#yn6+5^MOLgCmrjQ%#Pd>^=7?`X zxtU`Yvj|Ev09v#B%DHbhEH*~c9-nXcVPBCbSH0ZgK7GWYwHOXhQzm;SQK-mw+*EMN z+QKUng0>aHQ&#U)D-C2pg=3jpO`^^q6foHV$+{yI3g;Sw6ydvKR=IhUy6Y)7L0m?3 zJVZ!s{vbbG!--AeA68qqnUOnfjILb_FwDs1TiihE_w7tQ*DdB)t+9ZD*7jS!jsQVtA#xj65vdMkO$c2X35(2?>6uT#e zR$YsPv3Vms9HSK3`hvLQd8lAEUWi9)vdNQXrnjiC;fjJ@appfriI^7gH#qkjl&eQ}}~`X&p@iU0i%y^<5d#dg2KcUR-QF3 zA;)Q{oi<&JTHYs)AlK!2m8%tbJk7hOJ=AEcyR-h(rLk@ZK;JsO5a50HGHWe!3NH?& zjC`+{t))0so_m)Gc!m0}n1B{o4)9hyL819U#?GdVY2tco5hx5fo$Vi_u%%r_U^T)z zW(*qVsfq&o7y^=$JQFC~*@jhv&F&>=qK|UYt6lqt|-r$aMl~^6>;f^VIjqZ!asuok00~%}SnYlsQ6|8RH;1;_+U^2Kld6$HLAi=E~Ey5rbuxZVb*=q(v z%y9$Y%P8eejKfYrijGNaWL65raTu+^^(bQlmS;}}>$%9hUHW07%W7J)o6)|el7oyP z4vf51v2u94Vmh+}2U8)>S8+DTIcDQf5Wnj%H!}{+zrp}#%Fu$- zfP+iA>QyhYXI-!dEj>(Xu+3L8wQjgXz;(73oiQE~RD%W@Tsjtbc$C`gH7gf)o0Rju z97GggJ=CGAdwOP8)!rK1Lt@Q9HL=74SUWpPPWLE1{nT0;x-MbR?grcCU^MGFjISpe z#J>z?5ZkD}CV{+J)Eg9b>zSg9PT|159vE7s-KYQPH+9!UIyUf7phw z8<);v1>cO!QQhJmU67XByKRom5|qOA4TmsVU#JwdMaILjSx0AiPD-|V%PN>n=>a16 zJWVFK90who#`LzDwh`gJe8i!+ zt5IcVltF7)>k~@I=J=F1hnvmH7FE3FJORc7%%lO6=~Bvuj1FV)To<`VJ5j?>ZiU$R z26A13*ny?xy|gdHW@Z>%vibwzEa`63FgyuLZ_EmXmS-oZ;4BiEZT=?4QsDWD zt-1xW>2g*|)OOKe*_Ke=Z@tB7%NL1Y4bKaU077wA%xyad;$daWV@Oq$R~Qv8nL~K7 z6}hvFW-2$j`H8zLd?8heekJbG_?Ea)ts9^2AVfCu+Hh%p1Qs1148wVg8Z{|_k=9|b zt?{;305sj0E!F{c+Gcjv@QoW4cS1Jn!-?U<_wgI5yLtCLZ;4YLNsWBxwj^>M+D>B{ zv{RfyM0N>7Ze5-s#>}n#gxF38V)W?7QxH8d`Wxs{H; z%sTvt1=f-Vvw$n9Lz`F3MjUGk%(`LuBH7EBseGRGHEnu(mH9-@?>&a$HXEVfxGd9F zer339!lc({i0)v*-xCHfY33+ZQPc&BuSy*acQtZvM8M4#1fsiL<3tWJJ0sBFlXGZH z_HG-N3aaRg(A8r0N~}JfQu8^p99(Y(o_d)q+od6_HKVM{SX_53PM%3r1=EwMf?i(W zg92M-auj)j)@z!D>bN2=P-2K5`zq4t=3|3y zG2Nth&CK!-<}Tu|RWl10cC!n3va#Z2CnmhWh;@w1)D~?FN3kHy5I{Or*5)@}1H@`@ zaFE1Q&kvc72Lpy&t!|EG7zUQQn=Ur4E+`WQI*RpsBAv9I!SkC?(F%gosf%5z-lZ+T zrB@dwPuAONsAe-I1<{L1RPJ<4jxS~-JxniQo=M$v%&Be6L9f44orMpMorJrFr$`HI$MsRENmJQHVwXF zJ41s7)HG}tmuNGy=SV?pGsTv;8^$RO2AapYf()+ml%p65jEI*{ggD+BfmN^C;maH5 zHYsPCKwptHsEKs(k{|#*WsFug-r~3NxM8O{9wNM#SBwbTRXC%R3iPKOrVFfXfxs2) znlgL{ZT*nfwtix}xs&vpv*?4D8as?>TD^ znP6Hr8aa7HUh)=%Rd>hARtKZh$SWK=gM+@K8?S``yDqa__l8 zWzqc23*R!bzl+sN2LKv?HB=3niF&`>D#N?SQj?{gp?Dr%8GH>jYs~7o;nbjphgfAT z6y0(_T(I)JPS*tXn$8e)bPp;3JKm@HgYxQ}4Suy}>u->B}{ zW6XGB_c5(|ORP9!E@g#&WiMyAhSlqLD*pf()SgCKuv@NI1(YtGkOR}Bi0?J7;k1>n zQSuooX#W7UGNzH_Hg-osmCbV*^#}zp4Tn&pk$Huv>6l%Xg72A9y#2#a4-k}clqJ?E zjfyX*XsZmsFE?xjxqBeRwJNu)_ZTXv*Nc^LYI7D~0J{!i2%Zh{LJ6fCo9)5Ra@#AW z(TKHg!dy}{ISE$AJ8dhl5aBTS~2qivqZ#Mg%ckTsFrp>Gq2fUN_iQnhh{?F?h+NX zIKwwL0NW8Xvc19-BJix#xK_-j;u6qSU0h4e(~5w%a9La;Mm&rG+Ln(fmmANikM+dw z8RBR>ADRCEJ;tbV2y}oNS-cX+5cWY-w=W5GP~x$xn-aa$MO9V@nL$D?Hyd%zs-S5D z;ysZX9#|;_%Cj~GbzCySDbXJQJe!NqImsQm%T<_iw^}Ju>Sv(JTthv$Mm&n#%$s(N zOOBzGMQ*l?7!+-M-r$o9GY-uxEY(jCf#VN1G6?Ep+xVG(gE)$e9&MDOH-m||gt%$X zHNpP?ZXph?3u)2A!e|s1P|LgigEn2yfypPvR}!ReeNqFT}a0Q|R;RTAiv_*O{joz{9Vj zlbKipPy@S$Xk9vpr54Ns2Y1w>jW7V*qi*9EgP*Bw<9y!{T~eMVY!-3uB2`QeH#2$L z62~}Oa|JWHD-#1*Ib-8}oys<$<%9~*Q#UcuL7*v8@>fP?(s~RKMYUkdExOH>eX*`h zk6)nU96^R9!^F4Tu-#|z zGB3ZmUs_(KhHB&Wx3Mq1yIiZ?Vz6!%ga~ z8nq4RzhO>t#_y*?>4p3hqG6dSi*Aj>bw@Bm5Mf?hApXFFw#dd`OVqf zuweFwxh$8K6ugj*Ijj~DDkGkT{!*cEdNJ?0AM0-kr77F!jp+F1AJpLiXSAQ6l{&|o zy`6aW14noK4Am;H1owF(7C!5|TKSrXE9|BJUHg;GU6{uA>ZF~p;8Jz(agPxS#w3E} zb@DB#hRw9rfq>Dj$p+nU>@y4po*6K2ZhM+h?|KV*92o`JeLzV4Gc-Nmr1HyHv4M~ITjQ^78vEF@zHRzMUYHq!Jq;J_KjA>| z5x(dam>Q7PEe!h*RXpZcyXBtbZ^e!2g2e`Z*~}RiGCY>m1X!*p+2*I()A>AHTeK|v z)qq@Phs3#H^^x23DKu9GcRaA|d-Y?~L{xl4V99+;n9~X}-lur5Ev#49G03ss zp0*m2&ob_PpZ*E4@{Ua3Prk`|%TAMP8T;=lyRN@FpCr2AX1HF11a*HcqJUI3VRY`l zHgDxs8-qBG#1*4<-WFKg@!Ltf^p_Mg^4j-YRein3LVw;bx;1>?x`{S@)sx=hQTCsD zj!%rBZYBRpmzq^HkK@GBbVboR27-bb@b=fcG2SW#5iSYw_~H~$4MF^m1(Zn_*rcs~ ztm}zFJJ;Kmk9z>}n*x@Avqhl)GvKF;%h-kM0j0;ByZKc7jX#t}i8MtoNpJ|%>SHs1 zn=rG<*g?CQi+>xGPo&A0)hhk{X0gQ&kiyJ?H1w?7>lqmyI^dI%VjLq=5 zn)C?rnYaL*_2UoMJ!VGsdqsdZ(TOh>NnTOQkyLu!A)_J%Ah-MnQgCDn(L`OM}m^KH4*DBUqvd&(6T#f?qJ+JnYj5bNQH zk!&2zXExgmg|q-~wu{faY*}64m0*vY4}i^Fgt6%tE}WwLoHNo>i!HzV^pow3GwaE7 z*dLQsMCRJ=#Xq#9_#kkX?XLA$vDgLoZOGGIO8#GgtSxWz4>A{JQRYMzScPX|Z(aLk zx0{aNs})tB(GH+XvmjD{akT1hhXJ*{%0Kn$7D?4q*lyo*jD4MJt1meg0#u(6C`3lR zvyX3sJcAwUTn?eF3ySk=3rGMn3>A zch>oNVVimkI2$54>R{UqG!+SLHIYgziThecmOh?A?K!wKz3ffCkcP>Q72wz!8@y{{ zA*u0ZHpAKF#sTM3{_~q}MM*BHXjy9il^Tg8OJ612A2@S6#gS&F3OX@hmpWtP#v_aX zj7lrCKq+pXofxV#zxJ05Mc3UcTHNJ*n430NQV;rz$>}D)y;@nOs8(c>CBpsb`}YV6 z_KMhN@Q0~%rK+Q@mU=n1=d z=N1_VCqyHp!&DQMZYR8dM*Jx?3TX3krGS}6y7{%Q$7VvAgQ2Scb1}_9(E;has7X`0 z*xRC_ZGcHLI-4br^CYTzu$@X_veZQSB;n3f^+`OF@>!9UWLbj!>=x+k7s#J4JNxPWnen zXA?woT?u%D#zxag1S|*+adE+vYb*j55nSOryN>ue7|io_VH^}@q}p3r!^s5g?#JfX zS)m-FwOKBD_9)M2`-OZJ9G_z|>f*PcmESzx1nig)Fzy8|q+@`Dw{qwdpSyCWj{tHv6IPpiAo6dRqHwBYgVH79aS}6+hMQ3mP>4FLZRkdFU0#N?LS> z&m8EgpDat!ttjYBY^>MuOwnXz)n(wi&k1C+SrjQTU%Pv~C2eK-CwieN`GVq;ao2lP z#TNeX@7!e84cMQsmciqN1Hyjc=xHH7nNHNY=O;0CqQ|tr-(?wTv@v;R%(U?} zA)+)CabE-uL4oosF`CbU@+KeiIkPU4)@(ZNcWuq1lhOPAEOtSaE3FXbqv=(gd$%j@ ztG2A)5cS)DgFqJ3^aG25Pq1Llua%p0KC@z+Orrq z;_YM$!m%O0DdX76V#4KhS=+$D`x07VK8;1N5IQ{FHTd;+`~|BXwzDzP%=`mOh5(gH3Y+K+45?Ah zuUp2vQX@ENKkLl6eR30{~`iVOb9mBc!H5sBxh+Qa}EJ-zqJ- zQtUy#qjtguGVd;$qG`6d7$6r48}rx}bb{@Q&a0BrZ11|2H<^EslnxlZbw*bPF8J(% zW@n?DbljRBWa&XV@rvLuH9x?s{ph~NW6l>Vr-~8HF7s?du5VpH^Ll^z5VneByEY-_ z?HQ47LOg0u7^lUQY#F}@BU5|r&=V{HF7H2cI-G@p2dkc(Epk|!(^E5jJ4eVl4R<${ zbVWus7<+^9Lx+Ej8VA05R*@zb=n14@OxYe#!` z*_&c50hd|w!JBjZ^HIo)^8oq9n2T0@e+amn_XebWP@%y)Wrl6@=xoRZUgS>+f;E)=k?cB9#maL2Er#>Mo>w$ z*m7_7+eghfT8m%zuyo%8^A$;b^4q^!Y@Jsh1b4;(KC=K5} z{$=%FEXs2iTe6e>?{qMNF4`tOJdQ1Wqs_(7@4Gt2<0=k}oA~nF8(54#Nfnf-ihk+| z63fs#_{$yPDh6JV_@D;55+>W#zsu0e#q=@#9UT?2_?gt+6rf;#U_HCDMcNvsunq9c zu$W%}n7Hc2$ps8F5B&Gd!_YTfXL6SfcWveecPBo7J(Xx$AdHw)Vw-@oegA;3ek<*T za%fbjp4sHVr`;P2`*O(O#WvyJx57pzup$d*ff#T5Q8h_iuyOj&dvCT#(>6ffYuuu= z85T)}H2Ox5lsB1#Zs|a3n(*?OfoqUonU!8I$FpI`{Kv7Rnim1`VzTpghBTwn42#;= zVKX|^u&!i20A=v1n#$LoDyrwsgocJx7WNu-wQu%8jHCG`YJgFf>Bu|M;GbA)H!0r4 z&G8pwc9(OAw0Ah`JVjd4MYjEtk9A$gI-|iJ6KBtXcHRX+7QZv>A|b|=HGw&F?2Hq# zP~_dkY}~u+W-Ag06Y{9x?>NwzNOnE&4eY6TY|Rln2n=^!30oIG@7Sob<4->D%J78%{sVbZjkPnb@6iC`ccKAFFi<%<+Oc{=lqbV zNv6%#@GHMFEn!-KT!kOm+X|C45zhT90!l3Yn3f=gj3Mc8*`xZr&xB@6HZ{Kgc?_78 z`Nmf0Ii_PPV5_gz&Qq^@WCfJUdSU5|(3@1Z`h+_f{t-*3*DG&NDR|YqMHGXIARLX% z-vC1KquY({!_jiYW)EofD<<0_c$l&FFUVSj`?QOCsF_0qo7ZFPwLx<#@vJTz5&?W~ ze>obbq`+&mD#a@)Tc~IoOpNV6kB!ebcWoCupcCPLzzzC-7>{!7FLtTOP>)#e7_V%* z62~KACF9J#4=IwaDa8}OnRz)6WO1dT8M)0ZhYfyzxff~7n+}r0X1x7sfBWAiYx3$s zC4{|x3y%{ThSyJckzmQO_z&&9l^Kfy{HG17OCHi<5b61?7`@D?Pj7hi-N?lYwYygE zu8vRigmQ_V(1ta!dH( zajV9tw7AuGd%v6pM&JGRJ`>PsKUdZ4si(pCL)EhV!AAp}BVm(aMV_@_g*hC`X;7t# zUGgzCZMKvk={A(O;Oqv6ke&{N8`dXc#YEzIY+?0@x%at&~=zdUvg$y?^a*lz2Z~ zRN9Jy**}t8-m;l%sN8+4@LO)qFNlogb@+!ya^(8QT=m)Am2pi~oRCtp-dvHoz|OCx z{rC|-1qnV0HpP1&xnj7kJ?tuZ62EiZdp7xrMK~x(C0~7r_w}KE^THKb0S~0jz5ItJ z&q~o2WBwH3x`321*EDs0#%y^X3@-Km*75GaCDyP%n9a<^`&(U24PWit2@!3pnWF61 zFggNBbJ5=+aoDb-mNLL;{$`6!*hJ#I!4lix#;SWH?o-X$-%FYNN0G*~&a|sP4nxW& z5NG9sVI$WB`IRpW&;H=7WE)VF|0Ew_RtC3`O=|T!p?b~-vgebx9RZzfY~80)sf(SD zPMxKlC$^1Ap6v7jbAN`Qk*1^Pvf_DjerZKXDg;j`D_Ko&&Q+2}p}%V@!Bm*M)zGD~ zwa#7cVsjYXwz-@@=OfLLKhocPHW=_I?4jgWOIH!Q_oSDUou|0QK3c3na1Pa^5sJ>2 zgs?uU2VzVFCqp*8<>&HjV2Kn*GAqS1>-yV!iZ^%NXTmzD1D02LeA#jR$Glz_C!4nycZ)@eQ8er=5O&d`F)U$1y7 zLPJxs4Zy+4I+{sBf;YEf(=_(1NtCr#*#M@|yVA~c?v6*Hy5;N66B`lvD+)4JRBlN# zy#pY7OLNY>_W_EbjrxbiFc&ALAoJPUK*SNQPFd^4tJ2EKgV@(?lb%o3=*Y}W9rNlx zh_wE_iB~Q`)=TZlGW0h_^u(Aim7R5(TjTk$wiYt!Ah43ZtjcU-uaIYGXD07gXW)+m zxEqpIs}lH|-)7uA=YR{VNvsE)827~x!=^<>9a$;k`kOCvb-}bjprYa#`{bvsJzxYx z;Zy@lB}=fSrZE`c;OfPvM0Np6f>Icx7`=D4hCOy#VNd>59CwHR;9-(R1U=j+O--E=^5 z%}7ObQyeiRhjtkZd!DBF930OHX@$}^yYDQY}0|X|6=x$PAr$Ti(K0P+2J=I=?22 zq%nXbF-YNW=TQccExnSq7%9kClGFp73i9Rp-I!hD$cwOLOhlx5JfxY%s{OWQ`E1J~ zHo5WF+CN#$+g*H|TK|W(UshAYdpMze zK7n!RH>qI0@)C+L7G$W?KUWLIj|4(00neWm6as)Tfn2XjmNEA3#H;F-&_Dd5*B;ZK zA3#>`wBw}xKw64Q+H)4gkKl+j~bdO2i3`g4HJ$H}Pi3nd_ zbjBnx#w04PMoVz2REM*<>e%GDk+cFH_aoHv_IMz?#{z$woG+IUm zzd15zT)s9o+;jQmYUS^-eAttY1n2XQ38YOsOaM|l=t3Wk!hQJaehJi#ay=_}8_pMz zLum+tpEbhN%dC@c8C6-{ETwlDt|3;qf|I6lU>WX!>Nxe7?R+J`!;vTdsAO8w=(#D* z1KGACGT`K%Y$_LV*zC^=jz8szqj3%rt{ZGBjxU&Ou(euw)LSbeYM#D5_nx+WitCE+ zsQneI`-5k4gvRNIm(~FvX;aofOiF0;a4g&YBUxkGZ%1zD8{#-3e^b)98=gqBVe>a9 zqi>FG1Z15aFyU0ehYq>ewNH3!3u4ax9%6I{&N5o-J?n!=LrOTS-4K1V+=To*?)Fq( zIE$2Gzfp+@^59W@i?wLxvlYyWz)}7$qvf2Kpnt<2EWz%tIpVm`|0EZ^M(&C(l%5{9jA|(_6CAIA_{JO+Px;w$i_h(+I{@#dgc94{aS#J;hqb|Nb&gT_y4Y z>0T|$E!;?w|F!+-d7JcAj;7|U0k z=^ZrA{p<4oAUyq-qs}ue zI&j1&G!RqX?@OWx7vtAW(HvZ7wVj=edYukQ;xqsWVJ%V>LB8tx?fcUFC(VeZ?s}4> z8+i-q5t!Nnx|g&$JWFX3oYPKQ#S_2F<-JU*uSk9-TKGftdDf}+AU1f>YQg+YYR60L znb^p*E<3N;pcvnU0*FleR0y$)m;tL0R#(pA}4%&7>6pE7mo zDkc%nR(!S;;^c)9TyWh|k%g@bDyrGeABMeeEs-&M4y(PFK@ntqiKPgr!_Z+Jqf9Lq-S(9I; zUK9Pw0Cm|${cHN*3Z}3i!)j$neG+6k_9Ty6xf$;M9+c>^mSAQ z?2+UOnxaybo3VH`FS`=N+VelwFvfPI8ItOCA?U@FlfygkO#-q(7zdv2&N~US6}0hL zs088&Jr8!t#FQaUhRlNRQ`^ONbaXeh;_}S6h{2WNO`*s1;{5AN&ADcAc&1aVCOf5{ zeJoCQ3(Sa~{Olo?uxn(1Ps9`9An4zH&)~=zXSa3hsj3MLgrcKro-`To&#y<=rQ-=a z9UMf1#wB3T2QAi;anw{07Lt?x<a)jC?8Ul&xA=|AT_ihNY)RbbDu6bv%v{FRDUr5Wltoo@C^9nDDj=wUI$yc-UO` zEGWt+S{={D-7;E#%!NwDE*3%c_PsVAPRTa?c3Ix4iC%iSteqU-v~bD)d57SR#`>Ug zUMp<7ANDz+>7tXoV8A;}=g1SPTKK)SeqQDCQxvx$rC;~4br`BLA^mzpNbrOW%Ct9= zM?Yu=bcpf%v`tmFZ73VQGfP$sP1bW)XyD;c+R=>ZRpb+0u~R8$MfHJY!j{bd#BGvf z8wjt#rrrzusN=>udJZhHz0-I%_l``vdzlB%Amx8(B?vu%8_FXiA;Ct$9CcErxJIYTJJO?E zxSlNI72)Zg-?>kHlV6~Z{UB<16y}RN=)+!TWT%kOOrqwovynp2BO6_irr7y4Ty&k+ASCEV-zEXnoL)%jJ%Z3@{Eo;;Mt25!QiauxDIk}>w*qS zarYfWvPXBrxM9X`S7uD9#OX*Nu{n2!oFz>v(AZ-yj8j;;6nuJ-G}a8XDrsT@{8d_0F6lrt0i+N?#T zsanwkT~J$tw)Z8)w}J}x`K}7xE2QJ1HHRW9!wzB}m!+h{L~JU9E_9%8)Giy4LY*`D z(!gSMoA6w~9W%NQ{q?1T54noq9)LNU{mtcw}mWs>-ASu@?rE$t+2N8LndtE<}3TMp|B5=8U+z8maZ{I&R zPBuL?KLFQo5F%7oax2r3P;385np_Y3{U%qF!8Y;g(wD1l5mMvuB`m+HF6iw;XWb78 z3J^yu5%eK~x9x#aKwX=;6dtA?om!xz@(-=H;xCt{eQyV!yXvb<>p5aj5iK7RLSRv) z!E7AKjA219zLyNMvJ0n7z){S!?~PLd=liDuoB91CJ)<|zGQMWzZ5 zAORLhsu8)#cU**NQs6+^Vt7f|>?Oo@s-DT1|5s*uF2+c~XPrYoV4wgp&r3t)!&bZ0 zNlznI=&O}b#rk5X&CAQPTbY09nC$Xpn$9OKMpGL~gR&Qijpt2ht!#Fo`m6JxPOqg<3Z|31=6B^YN z-u(+cr|{j+PnpSqw-tY)oDM;D{cYyHRluCE07;{qyRq%alw^yMsQgJ6Krns58uI5K z+T6_iuKu&Ou^E=Lr@Ji$)~l^lWeuTF^`^sej`OC*W(7RfP_?1yM|IZgS=JGbEk(2W zT89Cy%;h{^xC2XZl~pf}vm%qDLze<?HT8=G39%~Gh2}6VIo)q#=4g#PCz+399 za}TGu>Zo%w4JdNyD6Ot1_peVYRc4&gMc<(*Hkt$kg}q{&-K}1R8_eW zs-e7@sg(nQQ|c=vGA^n<9w5CTd4@8K2{_)QT_>*@>otxj33p0t3OV!vAjR_M(dcsw zo9R}(e=HOTJi-RL{}d$7h1*>Tcq@cJ-7XXFcVu-1hs$*po3uaZ<8mSfXjjcc_T*C( z^Fcmh0l6Qn_fR$f!ARurV}^t-f}ZsiSKDs#qyd1!vr$KsCUQ*U<4SJD&8+^`Ihif__a4=kIa5#WinIOrB3ed0U51f(Gt@B0x zdZeo#Ahpg%-mD+DxrvQKJC=Ib9wV14; zEITA5W*yGhKMSER&=Ml3nBJ)E_-(CWVkMvU%kq8em?4D7YgDw0$sd1eT69g>zKkn@ zIyna8%u`mB&HP=q*4C4{tABrj_~esqeeklHkvmjrs!P5QzVs+>_oSN_YP1X;zEf=E zs8vv;pqDiT4l22&P#cLV6?9O`SGwQW(hbjheld2i7n6`r*2q{}JQXeTVw0X~>cDg0 zWCckL8K6A&5(@GM`3qxu`!u2h7l!+eRrhkUZ5;QMiDunXa9RD0SdP8KB?MhoSPn=z zpDjAUsqxftTn&Lm|9L!+|AjX{=F zMfF|KmJ&q@9Hla9=0NOxxVc8q7Q)h;IVUH6&ny1uhIBqY9!8Wv0LQx(&@o0ud_q-E zDAFs{O&aW7?vU{^hNc49fC|-&VZCdrf@wC*mr{~JE5EFR)icj;#01XjiYFPwkq`(br!tTXwLG6*NjWix(AXctb`3w7Vh4cG;JjpOyX!w$nCcJV1!nXQdvQ zf;rVMgE{EXyD1(^LXbQrv24BTU|&%j8k(cEYbFM9M7dpjgVW!Jp>pV#fZWR<%^=R< zN6n2V-8DA9yZD8vN_gp9M3`QgIMn#oX)1y1ioyh-%UkV|SXRb5?K=$jm7Jkfqi3kE zCmUQ^L3n7Mddbbo93u5Kl}8Z|is!-D=fQaYf5QpL>x(@P#tev?Rzy8f2Wf6SubAHK z=Uavcp1Hq_HVSB}RWm_P| zDgE@qFJjR$%1qBwN$bZLavF%!DVPFa9he~VN?UZ@ws=}Q)ek_@xV<;AaA-;_RpV8q z!%BuJIEmPr?Bir!TdIq^Zl_$SYFoMH4MPlxNp3WEhayuMe$YQDZRsllRpFZaohJ_5 zcs33yJH}qpk^I1$70AXzOx&BC&eG}GP0C*<#`@mdV3J0X?y}jI^P9C1xm#Nw$pi`h z3Tvd93VdQgdSWV^zTa2kxrArKq%W^<^4e+uc*6#Eol2~dA-j|yldnyR1E;8&;>KF3 zbC4nS8*Wuok48>e8Bivp8J~$VJfyczq%Tg$_>0{WNlarS9e&O#p7?X8%TQV5WY8gK ze%RImZzXeby@nGx6hjAK21(I_L+>d00*~t(QD9meb#n}ae|PT!LQs+zc*o|PvT>AY z6kuVlP9nS3RsEC!H`ip30c{+PaP}JF4<$>O!>sG#C2Lucp`bowS|fH9Og@%TTGU$l z78_nw;pFV;;An1ano?hA_#y&dZ$o zrI9Ar$#NhsG?^psw=Imjo$Vpz+zaaDj_A48OEl3JQBG+-aaKO3%1^Iq<9fi8;7s1@ zH4cg!?oxt(bPw@gp9UYVEw*vhjV{|=;Rj)#+YWbO-EcLdP@o&hBYAmgD8({!PK+^r zN3msD^fQ$5;s2l%v^UQv$}^tw<{3!&AC&Tpq|o!dCVKzz1CYUjpP1x7cg8nNvRWaD z|HDyU{U443jt#aVGU0DJ>ppkoeEvV)4YMz2=95ndfFE%LZ=F^brfkYX+WCU6!BbAn z8&WRebG44l5&i;owNxt5n)h?M65qkeqHk<#CiH|uk|XR@$QGB!F{xu^MzU#!_q&Dz z5u8WfN}$XzfrIQh2N@oOmsU`6{zFsh;1oZt{Ch*{$%X_eP?+ZSM{3pQgUj+qCa+a! zpSZhNpG>KLo3Wv$CPqmyi(?g{^YiP*<9rL097lv;+r~R)yy6q}HMBB_8r;t+Hxk+s zvVE}x=2oCXrVO@r34cngrTP%ElpPSmc&V++4O0>-$kWWz{cIXEJ$WO@*4&$)*yogh zbIw+%9J52krk13TwK2t{m=r5$9g(vGu4G0R)lxpB|H9<+$Sa z@$>SVOykQ)%8n)JOOj42|LoMf&Y}+qfaTXxUb5T@#mfI(H6dy3QkiDHg1KF<`UERP zayKzlEzNEy0bDmhPG-78tqRFa_RR0Hvpyi7&8cvhf=R_Kb2U__%bxq)-KG@!ac>Ty z^uK2J|7|Ql?Ecr>g&2*57`2}pJ@&8E)FF2~0zPA&Z+S?n_f;+cq10O-hfTP!Ki3K; zkphxaX3d1uVVzSJZkbjvJZg~*(@Lv5VX+rH)(*geKnG)luGtk2=U=rXt}4`?^V z=UWu@=2J0}@+FPb<=+&5Bp5*Lhyd^BD__t~v#Y7$?~kM5I+_jo$+AM>;xpFIL8F0VFBl=EzZ*ryWuSBzd8mFLo|Ciss<|!%LL5>f?=N&&7%Vi;80Hp`0n`5M|7^v@}b6U`aMXHx#3j%_e z^!0LV0g`zUKKgo_pyyb9C@mQg9pq#O_oyqj2Ww564dp!FGJyJqIeDVPh^+<-LHz82 zs#PP7us(+?cj?Iri-T`$>dSA~Dq&ynaFrYpDLEt?iJ<349e~3S%T`b~qc#2f10=a~ zK(%6hPpvw*^7A1YnNu_DO}p9F?=vkQP7$4K?#)(xHeqstnG{8t6f5_N_TPFnSU(-M zqYkG|pQ1l;w9+(r6-uHj!y6{%5>_OfrpDh**-5D9l+hG(k#bScBw7SbOfL({9|3Yo znV66SMKz4DMN%;9W|b&`S^D>2KmKVz0dkcwcMB3oyghJ+Q=%m7&c0nz{hHj_S| zIn**Jj+PBCBlJVDmDAEqog&rzAl$?QHc8PWX%d)BVq~PuP5tz4t2HCgOYXY5Wv<|Y zp-G0i$|ReC%;_>pNU5&e zD?@L643(w5&$P#kPtH$GKOQAds)`^vbYw&BX*`%mB5O~6%ri9BNjRxX9uX$0`Ec1+ zW#|WYz*)4uD{X{o&Swf%V>mdBE7zIVKH?H<-y;g@mO5H|LE!pCDrBjLs1TOjgE}Bz zNo~&IQY;M+r-xZJ&*5-j$b68hjsw5@y~AIRL^>z=y9k49;KhB@+&YQ|(3^b)!I{E%VT&ss)+@t+T9rWg^1Xz&teA&v z0O!_XoI|gpL%S|aDTZ}c*l(piNJrHU+tM~jU(-Us!^gQZVJ!vmPK=M6`A|je2yx6M zfzqC3@YhYGFcPyYW<7-ItY!;#RH7;?w$^*0gZ1#xrIR(FKH`h035|vbO5GcWh@KiG zPCqrO-rO1$7Kb;Y6s3J~@r&FefqAwtSO~Z=;1*jb=}R*>_WFSiHO!%~i{y9YB!RVZ zSy!-@xrwTDTfbjfbzh4IDCHMmT{B$PRgs39l7CYVip>73Fwl)t*UeE^I3RQNHb}Z*`IGv z^NwAgrXEuUHOkddm{vPL0?NnX!F9*8ZeiSuZ6#V`QX$|T6N3qfmXhS+~O z7l7hSjPWNQ zIex%{dRp!i_N1#MD9BgOC3;H`@KY{K_Mu4}Kq6#!Ky8_~^5H!)8dlOKo-T)n1Mv7y z-osZyDZDU7J&n;LLUk$E>yz7XV`ZPoQY#-xj8?qrrQBgY!+w%JV4wJ!(^JSKsej4x zdE7I5dXrhP>YtTqo`iqC1O`E(*xIc>tne{{WN|v997C~MsbQkDCByk_>4wRl#j|Z= zqL~=+vP&0+SU%S}?5$5FcMb^=`sB&e7Tq&SnAeSdAbYTx(I1o9{*k-O^3-3dk4U$3 zDCif6Wh*CTl!%3T2?7Ba_DLy}E7UL`#rn%Rl|_~eT!Bk5q2bzZwG0oWs$2ViOzSIu zhUZrxi~8;F8N*Q)8+Qs3QLGh-Y^?;bonw@_LXhCWWgDznGL{xHhEcru`}pI>#DQ2Z zz=3St(ss*y0Y=m*6<6exoOu{S85WyY&_F9cU4Le!F#Wk0=93uzxNH!i*ER3nY)P&% zw?yXO4`&ANj^!8W>O{+j;)saIvi(ex^D0IbH;Y%D3H#GX(^5ark|f#- zmgx(*yznUWOc%Fk6K`m*+`ltJarN)Rsf9ap=rl>l7%O{|p3@cEcT9P;81ga+UiIqf z>Rldl$`ZA0;6q`tS*i!3N8AP%-Ytc)spFwqng!Giqn{c;i;A%J6!63hHskk0p^m~5r<@Xk>^Nwb$j zF`~xS#b-42=me$vcj5_Et)dN|wuYK25BTW0OxbhHSSnRaFk~4AHJQyOuTsfgZSzhG zY14B?|DrugH$0muKG`X_dl^vmJ&$;=cV>y=#Nrw4I+uXYo#jVVo}aAwp%L3S`g*)AchntK1~O3Ps<(C zYEB}veH`H}5qJlo0tv4fee3R$$3ne7NGQm@IwqY9Y~{7KsusQ#@4vE_7xyj#cuc@s zF`VKt$G6BkRpTijkbh{j+25jXn3F!F)ppk94$I#XNPDQoEk$y`FgoKB;AGYo|Ii3s zuK8^930;#0TG<*AY9%{wwEm&l)^K+ig#XnCuv~yx73&vsamBZ+izKO~V!n`IL0PXj zIJ%-m;^I8zN;?#Geu~H2xn)etIjbF5kEJzFF9lqCL|+~(zV_1d0ST&k zC+Yjq0RGnVYIyL#icJ?74r8o+hp`VY?Q4AoethkvM5(K%AGwAYvy<$Pe60E?dnaP0QrK7O0WlG!RE&>0&AM z0hyj$Df5OUUaa8ZQ@Pj{fpL6xMf@MB@o{Yt_PQt{RrZ($F=XYO)B|}Mgczt)s4YWu z#`C7XK|#q^R+*vWcI4dV)?#afnq0~h;V51+@jad=Q#@_&fU#a*MX(gE4BE6ri1SK{ zI{|hC@NUVW!LR*Jm12udk8)s!r|z!Fjj^3+@xc>zgAzVNj^sy{Ih8t%v0m;PgY?M$ zdkOM9%l+Ti!RowqqPRRK8+D@;gC;(olbj`Lx3++{SX?vXTr+kGu<#^$iAh|+=F@)v z^k6Ih2Ta2xPC@gy(O|r5%P+MWrGc7zP$T%>(OzV#)0odB&CwrlY)@ z2M%5HSh7eYvNl)|1TCQQ7^UK>I{%wGR98kj2@<%X9Z7G;@M`X6A6WZf>B<}QL18xONiO%l2dcCIJHA+)FC`6j;z<5 z9%Y;e**js$L3Ia8!KN`NK@B?`9viCd%tHXkrZyxzckl;X`K=k5}9G>To*5 zB-1ar{%PqSS~G76Z^@m|GYL@9`_4qmzb~Syh>=N#9B8fei#QqpN}JSm{f399CFc8Q-{Pop0xjHYd#5+gLK5@;)B5E0zskT1v==h?!UWy#D|Nfx`v~W0krI<0VTSmJ z>b}y_lT81^f;>mxU5kBj^SuzA+!a^DcO!8|Iq~sHVJ0AwfV+g7)D1WN&F0H!vwE)( zSa;vZtDK(nWb8fiW(iX;|C`OJ zk98UN$68cw0Q63T0e5f)pC^l;ZJ6&WB)HOl6J2IVRJ$%@r0cX*7gdwFq~&L z3xAH#YpnOHw@tBid$Ub77n^MPsmAf9g4h9 z8KEq(*oJH#jtaY4_anHfm)`f{Z@>LqItY3f{)2EFdeBHX_@B;$Y$0QRbE>=oljzjj zvZ3zyz1ckz|KY~K_*vBSKeVS)4FzHB!LYXjPZ-=+RjvoRWk&3QK|PF+-ks{B5bO#0jfuw9hb^WP z+oIhYtSurdR$r-wF<`$ZcT`3`Me)PB7{-Jo@V7c&~3WZ?#uO@ayKB)Or$MW=t-XV5T!n z0aHIIl@0Q@5@dG_D&yj=fJZPnV>l7|Bqh>cYi~?qyL-*?16tuSbVc6smx~$gU+_&g z4RwYyi7L2x&jL!q?(s3VziH#(qJ6aR>G|k-%DNJH?JC&XLqwE=(ev)-MB`rwiJ@Djp+?TZA32}3`KkUn-#RMLXd{t1gxGv z?`iwtDK)1cbHQ)|=AA7T3qUh~PbkQPiY=g)IIqro3>N(E^n(<;=ASEk3OtD|fjQsD zk5*J|lgq&%r^06fZ+j>3_4#qHN_G6ofER_DtzClQz_!$*%uIE^dKCBEk&ftH-DH7J z*kB}7aDrj@i`{d1R=zvan#)A5}@}tTOiLXZShHHUmkOy9Ate zKd!?Tp9N>WUixXV{QQR&*8E?IU~9`aLtWxi7P`#-HLO!yg1p~-QppcPy+=P$wZFb6v<@5`yF8}l{_yfL^ByNuCc}RA&mE6 z(*^xS3(%APoHo3UmrB*{RY>n0RDU;;8~y$G5!YOXNsari|{Ezj6Ulrt_|FqJkzbYAj*hVd#tKOyv{=CVVqmjIPb8}jN^HA zHXT|7PvQ^u{|A&nYrmPtjA+Mma2jl7sCF1g)`!ymasp~U>j^$x_+{o4>Hd02?Untf z@`cG46M^=T9ZzRs6v4qP3-w4ub^S1*L!CR>=|L#!Vxi^!kKE%w7h$Ai#H$9?FTHrys^aaG-2EPw?Y^ zR#M0xNupQz8{&wjZ%kRD#&1|a3U&Wz* z6^$#JI|<+sdJP<;*_E}7?|UK~)LI{_-`>C2U_VznI`cD+*eB&D^hdzj3f9Jd0=mj# zveAy)O7-H#Gbug1DU6H3`EP+i;ie)cG;eeQf6N9V z`Gy@Wm(hBdP|mP%WwspPduM6QqgViiVUgoa43UvV24yF5O;U#{oFwK^`Wrn}5%RwQ zr*uk@Ej(UI!992uAV1LXIRHT#704nX%aQW}Han`qcT9vnuYE8<<6DFdw&0QdraB(1 zv9vsvsDP$xiQP-w0-f&k{Y{U{@m&X`lZd?7Jb5B{E9f{7iMsf~pTQ-nl9Pn77CCt< zpN-;SCmlq9a+k+i?F(J_P z!r_JFE=gf2j2FF^I9I7cKb(aentAqbShqk>E7hs|f_z>0eM_K&!b`p~jfJR%sX!uh zP&VkTLtW_t31OkRWW0jjM?*goOKDe}<(ZqDPxu6N#+_Zgxl{X5_^g>irBjvVXA)Tc z_I-m>%nVGs2^yW-_90ez9?VR@1*%&YX_bD5c){FW3Z%{_hSmQ77%GoVoLss}Fw#88 zFS^-@X$gmudJ$X1pTdJvVJxLT7&kMZtAdih3U+FNIvL~#`)tS*#w*^ zSQAiXdIC||T2e0}u|(X8$}1x^Z%htqGkmTq=YXqoYrliehH-B zXth%f==RgH?e|It_yhM)%$+~T%x}mN_(OCbtNmowV)36MLTOYqvE{hemJ>XXaAF&JUH-R~mDzr#`K!1rF z*PQSN^5$6*3aKe#af5V|o{a$2BhKod*)FZe`)|>I>U7uSP2U%``vc&r%j$l&AszBT zoOx6Ro3MZt@U(SOD;P*+YPlp2LUjm|aQs9|p$-%H`mDR1E4E=vFT6Zw^Nb5LgFl|p z&hg^&24I&W$mbudG_jZg@U$MX4BRqs5t`KoyBaD>SS6{{7yiAS0UqW+hR<3d@or>j zGF_Mvw2Z8Q8=w<-{{Tq*W7Dz%{{Td2`=3J5qBWE{zoG;=62aX% z5a+uR9a16>q>Chx`5snD1LJ45MKCL9pJ5LkQx!vwl)8UB z{nV_;j-NiT+T!1WupKBzO9mpmWBPxg=|i7VGJoy^;3t-(GI5B*nL{3yl>m^lG=c{} zYDU8}5RB^)x8)@>ZY1YN*YUXtG6DNKyMl%l*oMC1C<|ZfB4PbXQXj$a!$ddeeR(3K1=^DbpD7`j z@Dg#XOLb&`efLoD2T>XO3>Y@!h=SO{5Em{CFW|FMugFiu*D+Io7K62lw1o4k`q0p{ zO88li$$teQDS3|%{(I!JwS&TOkhEqx4{RZ|krHV3-9#8MMHmV0fiW_&hjvIxTw~u* z8|6{`pJ<{WK1+xuc|5;>1%vw%W4jZ*1Xd1^K%?^zipQz&3M3xH04fb5!U*RBtA_mm z(ryI`dqdbZw_-el(3wW(6Y&NY!btkfh-ohf2aj7V;|$ail4zLq{uKHNyJS0gv0z!P zq=7jVfj&B@e-ZMLHx8cQbRO^@+>(b2V>w6V@{*_e;D14A_ofnGf_VOIb_SEg4~ED2 zUHcX-C+@*!lr)`0zZf*FNnY>5B^790OVAyG&u@e27O4rIBM1wUHFg9{q;{!*E&LK+ zc0zE02-)09qdff)9rLYcF8X)&zcmCzQ8W z^(y5o;Jp6;i3VAYAbAw>O@$U8S9oU{lIp>Q@_f!vJ&jq`^vAeu>+FXt-QQW2=DLMQ z%l0EpCYmD0)#0n^z-acxA|P*mgoEY;VIfhnh%XxJ#DMo*2##cG4e8_bl=8!nFmD}5 zTuOrGRfQyH83xo$BLg}eL|5!RT_-d;AB<7K-hv{|$yZSFvXFc~lq3aT$nn>ZFz8qa zkf9=CXn-7}w7(J(*i2zhgiX(=0}N_PhW`L3I^}o@KQ|rW!x5+K1zaQQ4a>wRuNpgN z4&{2gD^%xz7T_DXf&l7qAR5(xLzcD( z4SNxHAf1W{?$-{fMux#4$VN#(`q?El56p9KBGwP6jD|s>AboNni1!XaW6eS;3Nb9zfpfgrcS?fs-SB(R0~a7gr# z@DoxVF;rTk5>h`)dK6pPj(9+hYUZRlz z@E;cvv?32czHNt31V@HZeh*UmgYh&g+B70cNG-@wNdy8L-}a(ksBioV(yv0vPJ^GH zXljp1L;i42^?=&Iy9)AeLLKTwCiQCl>k&}(c9KI@^$wMc2OM)5>7+tPjwHa2ek<@~ z{ZSwAYIZUXtI!5#Rf&c`A^49~P?oTJd(s}n!U}p_8>vN=^p#m20Z$<`*2Q3Or9cbt zZ6%d2M&%J#SiW`aDSx4rJ^?BT%pY~Y4)cM-@)0+2Jd!A)J8CkH`meCHN0?ZwYWE2l z5}Pq%+p!Tptz;r@zs450r|O8uKUdKG4CUGfQO5>rvr)x=B>>(76!2JHBa{*_=;e_Vi4uX6@YJqm$fx zPx$<{PFB61Fw5hmG`}XIq9O&8_Cdm+oE#I+1h}X12~Qu(w0i~A<+Wjg05vm6`Gdp4 zH$fxEuC@p3%3nE@oR(BE75mZ&>V13#5qR9AhYeiOI553SgO9uf`1_W7(?7{(+hPV3 z-9}>IDAd(Jd9*=T(v$2<%;^Cygao*JB2pH48>1?R@&&t4w_c!ut@tY)D_Ty?j1Bw` z%^(u+zpJe7Hn%Rtd#Jj_y0wSvI9aav@>1Ljo+kCNsoaCC8uZ`r*R;R6w&VVU-d;z? zKjI^USNZNdU+w&(o)k;T%a~j^ItpRNxu?kCk3iTcbJONRxNrU+$nWWC3THn1pV`qq zACd}>+T!;_v*BKs1<3G=mZtC@O9M|b**SCYis2CBUQYl^iu3nNyovB7nid_7-r^7c z08jmL&iI}Ej3laMhP8ki*Rn)AX%;Y*tsR>X4{_k54W1&Qd))yRdCWnF3SKDiS3onq zY^P_@1q-B+OcSLJ2fqt&y^$Um2G{3nY&jC`KQ`_0`_G5RhZ75x6rj%j3bfA2k- z4;lDK?TPp9F0ruUq%1>#rhLoua9hfWzhflEDN{)xo)_EU#OW(@DGR1M z$ufgg-Z{`N7X1dtgHh7Er0)uhp*T(Urhm>!{1fy_5!qmQ-7dtBj;^hH{0VO=)g>qN zgV?l%QVov8jCk8y0Rq|>alF@nmfiw1B)_SVKTz#Mv6^~;U<9^pF>7(qFAxl)vI|EBZeJir8!)JBRKYBQfx69j9^Y(LYWK5-e3d z8~a{lo@!?(cJ>W8Z(TeI_)Tv|4v%dz)joDT+4g;34=0I_bCAN-)5me}l$QFh0Ih{# zFP!o7(i9tz?o6mcZ(+ZW^g}WH_8${^lOfNr?CF!^`(bSQqM`17qoG)!BjnP}g-!%& zQ1KjfUd#!8OheYcAu;wvdP9{$NiSV7RexPe*!) z0-jy@4Klh;4-9aCJ#?t)p57Po1pGGa)V3Av!8N#F3QK?Cy?*IOwAbpC#}}@%2=SSQ zA!pJ^X7Jug?+X6_Rk-id@V(M}3!~a9;{1JOnu4E-Q|Q-{dR@h^ij!;^k}JVsD+;zM zJBwDS0y>DF>wtK0UQnb+&)MuEOC5dF#|F*Muh)KQc!Jskx<4!NC%t)LnM`?R7kgzL z)Y=Zu+5eFrpN+XIFn3XL#-TuLfKYaLD} zGaW3?!2FS7P*KewE+f(WmHu-mUmvTuE|%mlQxQ}zh)M>16zLon@klzXWDULGh8_DS z;tCqAUx-;G;rtqm-p@=wM|pLJyN#mD@tyrPN%KPaJ3Wt=cY;Py73<5(vQPWXu6pvk ztOtYmn4v%MG3#pJ+!r^ik7Eqn%ZHZ6+Ra7|$h1wE+#*4|U)OO`5^MZzE<{3qU}2|;tEj)i zHXr38hz|gbUdVTI*06+Fu+N%e?|knLx2{*V!r|!u z00ZP@>Y{jY@WcNAZ`bL!fSKCpULDJlyiOlWuLSE|ew>qmP&_>OUy4u8{;V$qoK*q9 z>^r00PDrR1=J3AwZ{=rbef9VbCp?ZG32f6N5lpKUYvK0f#H-RP^TJ7Rx|M~H7aPLl zORq#-cv^oqlZnkqI|*@M=wLs+QS*$=bJ~q`xE%+C^0!5EA{)XjX3(7e!SXD)kdIkc zN?5&?K`(1$DdN+KAYbu5V0au!KcIo}1!9s)4f2wfAA<5f_B!7>a{{ROKz`4M1uoVNge0u)?^WYt@!lAFrL}0FJlGLvBUJBoT zi*w+p^%aU2fv<>^z=X|zNbXln7C`m=@fj*$E^zEu=lg;8{?yk%$OqSg$Lf~?<}wGM z{G-{k6G2S6nHwu5F-KDYI*S@7A2v9YV=*y@z*K2xL50M@*yedI1&WZpSLK~T`RrOj zzXtyRDvknh>nVK)-{rfVg(P2v+cwxb6!2NESLI3J+41)E%Fa zd4CA>P**fo4kWJ&p)qu%h3|i!;yhGv44BAK=i#x0c8$hsY$`R0>;7sgurYkRgP_cZ z!q9b8<^&Ff$?#!&;tRs4dLfd{U9qXRE$c>8`kdLcF?~R=UZP(CE51%0|_a$YeKJYPq{8lj@J0e(82O5sd4FEfx*fvv%c^mQ^4$kh*Mf1FxxlW+?hi;B z1|66ubG|F42O-Pdq;j%+e;k*>(}Qsn(*FR`?gBGspveAQbwT;nHv?Xp!WY^u(?LOb zg+N#8lm3Z*36vvdYN_AoVQJzr7@a?Y1=^DWJtHg=aek7EYhcrM16U>Vf&O<{ z+rp@}cS}qiCO;f{ck%jl55NF*LbCo-V=JuKv-|pt?w0D$#r#!%H(!7#U22;fAi>Vgy9FN0a zE7JHLPsflW?#YA!;dKvavEAMgYT&FDC${)5U4cEQ@v=LLVcv?u@r5lUprcJU9wWx0 zpogTX9GFH{V;n{p3ju^9z7t}t^%hMZOy3z+qEMyjOak=0>wqlSp7F)FL4~d<#Q1>H zcc7LKVP8Ni>ew%!56L&l9G^%%2l-5-jhop4EL&5GyCZ}#Sp|h?Yyk`6toK9^g0h*# zi;Wi_`20Mm`J>V}@_yV(xg;-Yc1F(oj(-V_#r!rmKaudOKh(8ruRReuv(J+1G(fVD z?k*Sq03UPV`0u`4I*Nv@`zuaz_}wgKx;k`17g`{EN5EG+{{RdIu)-a;pari)#>Mb@ zb5Wy&`dO~8--8Y};l%t?ON6K`;`sjnPhP@zbojj(gE;TVLNF)PMY0^HvgkQ&I;bdqs5`);+(RyirEq7mdmk9^zp5|f)egcCuJzw zEGgu)hKQfg_C(o~Me?u9qIhm@;B$h0ZvNsC3c#l@ihqe(sEK%ZUMC+0-{n3Pj_Sx} z{{WYHBERk6;;6rUc{svvQD@-4*mys0r_ZS3d}A@Z{vC+o@c4cSwaqRU=GfdgH`}K^ zK4(|yLM&KU!WZ;ZK7WF(gWg~zteQW<{$G)FTg%2-m|YIQTI#!uDu@E!mo zr=&+f@(Z*I;GM+yt_q{33A#^T+D6DP9)dw0X3Kk>=|!siJz6TE-J0 zc}Ar2y=56GY&Nd{09-jPGQmj%SZN9*FxMRUUqdtlrW-auL&9Mtog#(Kgv6+wQFxCV zob<(e5v92o3o3*{hZ--z0ho(hssLkuN!to@u>3X+J%E=Cb5N0`{sXV%pvzd1D*A#d zUNziFG@e7|x|3tZa`h5CBJ2lh)CMvzjl!K+$gK)elL+up(?7N;8h@i9R!A5-;*bLV zOUB{qG|&k{d8hzm8lYYnDu^T2J)Lk7g)Qn2qCR=>mV#H1vYpT&;BS!}N6%FPs5n3m zgXsb-)d(y}eUkG1Spdv(OGv(=J=oD!H1`n~aRl>AP`s<@`fjSyKE~T00vlghjxA`u zUj-b@9PAC{$pXECh+5+sAuCeyxDW?b;2In0NCd9{;ax*k(d}g-N3?IQW|UtSp)9a{ zprKEI)jml`W5pp4lK2K;NJ>zTm^gLfMqpCtdKgU^TF6W<{De;e+<}#XOPdHpo)Ck1+6V}KFcU}6Vj@NstOG&wAZMiN z>8<#(2L3YHaVg1#~z>v7uihHi76{YN7b06l|#kjJcvf@w=g=qgUie zG0NmJ3PxiHrK9cafV&YRN2k#%$Hz@q75c;@qb8gjp&axRl4Ss2>PzPk1|ukODnezL zhdLqC;r~7Nc<`!8N}U1yuo3a*4qF`j`O zkyw!!7bFfpVgo775JT{fUu2Qc?kTs(Li|y%ZdCD+)WU*-u{=niEgBbq0l+>5j2=a) zRBB=b;6gqX|$kUobDJ!=K;?bpHU>F{7<+8feHTi}%=t!o&E$xKgh$ z$S}i66~*6C3zcCISmyx}Gr)bH7?93<0s-P}zzQ#j0IjVHy%oclBW9@CCWD5pegY z*brxSgaP)yFb*vs7IZ!o@<+4&qSo%{;N`}Id43=azw_aXb)7$7q2aLPuw0fz9%g2wWmkKA+4%K(gp4D+B1%h&hx8+j2J#Ht0(SuW*QH zTrUwV7@MF0gkmKqeT|@x3|GjD1H1D;+G>G^3^xVqLK7(&iq$R-P_;6>qWf@tRU1UYvGdw*p=7R~oIl*@5T#ip*XAn|{-8Qd)Z zT`n1J8akNo1OnLnMc^vH+CJ%0gxdg?{Ky5u%1A+^42^K3&92%eSxfT>Y*Ux_&|C$h#}*OK~V7PW3-&b zO3fn?X-I-ryJ9O&U#Ka3DZ3be4+{DkWC`4CkQ=<1nXe?T$uFC6fG$|X9Ct(^UKSwDAC5>i{lZYJ>71Ba?8OTg1X>~Jlg@+laJ)Sfd5Vrz#>m`-&Tz!Hy8 zpA5ywu#J$K5CSi3EWmHYHutl43B7sVGxmh@xq9djV=`n)81Li)+VldvRtG?eL&Dx* z5YKIr8Egxb!wYx^T<95$$@%9lB=U|tw|@k!t?U51pF!;{^WV5isY}AFmCur z;m=ehpR!(WLMcdB11x<5%T2mq{w44ZjREa>7SQmfg}j>iKBeJA1})@}ZER!%*gazy zIyyAb^Y&s672AS~a-KqSZ__3PeuD^W1p`O%3~I=14+tv;0^kiTkYU?NiwC99h!0kki4RfZ z0v}oO4A*EuiE6P}Ue`tmzW^Cok>;~P1ADb7?E_E+di^37ZRk+6?vSkuxg;$B zxzx#83V~xF9eKb7d?K+_6DV-={{TR#j2k_n1g$f2NRtPPu?69tq@k+3O2oIoiL`$} zN3CiEjCQ~(1Wu@%&EXM_GEUldiV+W$QX*6BWEa4GZHxn;JA!$IyIG4bv_!#Eu|lix zJWi}b!YIZiBG#xRMEXCFg^9wBEpR7;$1_#>yNBoCpW+aYr@E?oy29V898T&&%EvFD zgrPy(W~mr0q3PSr)`&THV)>#$U_jC_;~ZKhz^-os7nIrp$>NIPk?$xG_P{IJ9yTff zv?!@cZt4BaCEoxiq-$te_yUphnGJ(PX%CM32*K&N7Pvo!ZXz4T-{cWFqL+98YI*@u zk6(lOis9Ts9{BPoJgdSaI(DY8dN&}fZ^c*PDI9wq#I~2X~>j^CG!yqcjEz}jbmsF>j0!LJJ1xr5#x;!DB&!C zE_ckMSLGy_K$}hEBh{KzgcyxSN^2eNq=qGE6+?(}2Zyb|B49LEn1`(v1Y^aERRe1X zK3KuFI#6$x0+?Cwy*lJrJfFylQCC$EEHF^7*hK=!Qb4i8f97CgO{502@fHukH-JU9 zE7#IRp`+-b6*=PJZcsQi38#>T;T@88`%)d^n`=+Yi7~%EbOGvkhw&X#^7viEk~~3# zrp2lR+2^9jT7VXRq(&sSR9Du-l$QW_EKyizAuLg@xlDHIwz$tB7qUEWDMgEQp-As# z6GkiKQ9Co%Y7k3O3@?oZ+wd{JkpjmfM|;qT*P$}P>7l)Y+ql4C9FHDR!o|xzaIN7YA=JAbe#zKG4Iv@WjC0wogg( zFOBD=J&>=4!<;>+XDf#`;sX*eKcF?=i~!EGU<`C(GHcEJ3h+#vlu%e%s4uwIK=20- z6d>?WUSkK}^HdEZ#7k38`d&}j@9Q2_aWph3Qc@9ltiLpHf1uIkegr-{2CcURKkyJC z!{RiGL&Y{kF^>*;kVEY;hAQC&zs=O)vv(3R))vTX5`Am&ee)9s>jWLW!3y0~kol3l z%2;orLu5J^v^%c>rFlb@qmQv5zvy(u`mh&;Jr@fLmVA@>G+*S9ufsS&1|R1t_G6YL z>`TGhhlE|Lw9&zjB1TZqp0R^a?&|I+^3P+PW&_VMw9za_N9{h0z z=dvJ-MPa*|w{(=83tZBcrOh1q(FP;f0QGMe0eftUxNlED9Pb$ed7J@;DHLR!C;8NH z$!jPLccg&mSPKf3EWNf0;oBi~sH+ID?~kl4={f_aV5f*y=o&oX7O^Le7=()FvD<;R z)`E5&S4YnUA80EB>|TI$wG?;5t zSdbTlJ%+lu0Q$ZXv3J@cC}>BKV4ov3a4A$+WcIov@+TsR-8VgCT`h-!{P z`$O0~Y5xEb3A;2Oz#$IL20>KoR5yNn_b7%ikJK~!08A+7(J%)z-RT_46F}OD>yb|2 zJRi(KfStm|IBGgdhBrhIWbQCd1;%^;v5h4L#xDzejV0m*AQhnwV8azD>UlTFaNnq3 zL{FH|NDv-Pi=j=7)FXA7>T$CQz2a`_=m?;Yaz7@r=?eefgd!{M= zOv5w5UMpOvfW_{?eyYnrm9!)Yj{zOvj=238gik3ZKsbQZr>oxH1iT~Wb!sJgM}yP_ z44#etBFIb%O7vOiSexKHPlyAW%2*x^27uIE!)9?ForrB@DfA=gxd8*WKn6h7>WUtZ zp+i4-f-f_W0SO5t6GK^i7fNhAb-F-zdZPYY(nmhwgLT%>!X^Np*XODiD{gYs){sdl%F=g6P8q&Aa$>_9%k z@Keo2z6MezequT=;xZ9J4J}|a=@G{RDM^*QTMr%(Az*C^hcB=}vp$G3M?e&-pkj2+ z^_=+1e?frsR=YvO<1WI!L_GeCe^0OyiXeqSm(fsOM4iN7vini@(bK*+UZOp+ha zw3f|p7pCA_!9KxwvU)-#X&b<7Hq3!o2wg{r$ao@INO8QN3GpGtwO$+#eGHNt-=PC7 z)op$i&&It@WpS}`c=Sc2_bT{r0dOfmR|azFG|9^Y={2Mu2-h1G4bjx<|0G z{UIp?^UPCq;%Jww=4E+^H~Yi~H1aCV>|{iCMbu^|;0W3<@U{cwZ6hnzs73nQ1<3UFnafa$sVX7VutPSTBlu>E*99}L6>j+rFsCCBXiplHe{CKdNrLaE%Moy6NhEYm%9}D*? zxmmzhC7cYe=(h>av;rE>V$t{3q6Do;N!CKFzll*P9?TaWs!)<8QO-Q-JlS0J2yniM zDqg`dKdSJim4cH$-BolpYB14=sj0p)k7&lZ*g%D6p2I;ff>=0}zYL#Q)^ zY7Y>Z=sllJ+Qvh3E0L*Vd^lL&m$e|n@Jq~WOR0_3;oxYfH)+8fgtSWLH1pEec^nTwKJd>zL6Y?L> zT`oT@ycHggm6jnKYGI1Hv&5i|q zW6!7v40+HhT0)K@zOm6GQEEYfDVk=>f+M6~OZY94f^}kP{{Yc1866HltL@XDzQolr z;bYGwBjnz@@SI*|L=&t}6BEoWFNpAO;fS?UjXTpFeTuouoQSp(HH)T9Q<;sc&k)1x zf}nkKU`PM-%dWl%s5Jj-v@>=4xNGC5AiIr8zX-_#b*z*eN$4?BYNf zS(LzxL?asXgW5KyQxL|&VDu&N)3KOrM$T#uRr(LBP{^TL-yX!thw?vKJ;BVfQ4Q%jI25RI*h~My$jUoaO?Jl(pf9)y_g8eHS zsLBU>z+VgIJa~b4wBjj#qaI2t(oIQZO#Jdm#iiMKRTP|+j@4voSR_S(5|ktQ9Dfnj zu?D!4+rFD-$PF1bF&@!(@2-w5)7`^`R%F{{S+sfPEui zcd<&;2bD6!Hu3J0MYC~He8{KLg4O%BA>kjCD}|DM#30Dd2sOC_lu8(^aPyf(xEj02Ai=935cH`)067gvMEiHe#K2o5 zX|xHv3;0fe7>wr-Ire9l6IBq5f*w(Ti`lOu-D-Qj3nYX%eLhaU2{ zc$z(EZ-rs-BU73a%jM6+_J@9MO&jRn57cHEh9`>K9c{l!zVZZh6d8@~N+i$mnH`H=i}@%|uzVa;G&4AJl;#OA>H$g>fLz6DuS9K%9@`XE^u zI;o$@buSer(1&!8UJ!2Udx>*Iv}xV}1U{6pL&VDr13X)5bW0$7?!PIqxFe{Y-zeA< zAa#8X)cipgJM4h@zpw&P;UPc{v&D1L5)UYTN;cb6sc&i6HGk;l6Z+b_(K%2 zn2i|~)FO8doQ``;ygc*hamI~ z2x_qm9(1W8?XY6oAqo<{!rlP&soEzHygUrK9ut>`qr%)@HP2VRbEADBPXmUoF`pQ% z(0mCqLyL@yi81*_aEP!Q@~Tp<sU3GPoTFlFF#72d}URi~fOR1x4W@Rgjh5$TMK^ zHYRdnuv^CP8uBzp5aXL6<`!*`mD)_5RDR_0Vq1FvykY`W1TFLZZp#jVM0I#xfRZ$5W65u3M zscs!A@U{;k>KYHa`T3uL4l&da8V8a-vP`d_swVLBp?b(oVu_)j$prYyKS6E~M-GVi z{{VJdhT|;#q7`r#FlT-OMsfI;4a1h$OXz}pTNABTI<+lhxWLwbx8c9_8^zT8TojCE z9qDrC<`h>;ih3YVQ%OYtw^BkWS1u}&f0HcHL6h`CKMizVdxAN0B=s~$73F*t-BFt% z7glX_${@SwYVNNzlR$s?Q#aC*Z2uO?AqE%b@tVAVEKbMCMk0|xM3_MeO>5-6I; z3gNraETc&DAsZXvl!Kh%7v)*SxxU5z(`YOf^})nAOK|T<_awpA=#X+Ek~FCx1G;*Y zZR?^nV`hfSij#g1=kQG>6SKp|9oZASG5)nMPU^KPFm>c?B=G>e&HeFGw_Lcbg0z_wbCB2f}K^k|WLKI>q-~&eHXCAIsqnql8bP_m1p9In$i}LEMHNt$QfzbG~ct9&xF?y^H9t%0b z8!RW_7vi9_B^ZscG+!H7O~TzFKh~5oWPjku!pY(Gql%bzb`cLyoV6%aL-EWXXY|w- z{KSik?!@w3!lmXO*%@}=VMPI=nj|+3FW_P9m z;rfy%J;P|+1392p02nC|Z^qbNka-(`E_>Jk=QpBN1=pDuHyFY5h(Nm_sPI-qz={`= z=-nnp!)07st`3ByB&r(Dk&GpIc|1zSVPXWlBIC7eW7mlYs<8|QOl?DBaS2FVreZ5t zG9en76#S^1UR@JMvr=?3q`jjx5)p(sUDD2FIIGQ5uc%^5WIh4(ggni2mxdr zN0gbG&0wJ1f+Ub=b%8PuXuL%)vE)aPH2B0rV&w7+^7SCeQ}taZcOLO{-}FBy)9}AoRX7t5uVCKF^ zA5!QTnqH4((2_9I&@RiToQ4mt!3N?`AZ^{Sg&!_FqxkRj6g@((+3)`VqygjpvJ^4l zg~}7~!bTvp=IJU{JcV2>nBR=^aXwUTBha{o1b#Fr^BIrg74m49X#MSPQhgQ`&!`C; za=BUf_+JK_4fK2^1D;qEYYr*AmEy=`HHPI4`P2X|`vwfX>I=^Nr?gUbG2`J&3QQQo z%p&`<0$m{zdj!aa?hPkfL8|o%7t{~j(-~WZ`2K>-)xWz0h7>EePWpH#%aj!<9#7&U z?eIhCfj<=mB~;c@lz?$SC4RKp#W66aJ8$wMTvf{1l^Uzgc&5$cmuS zwF3Bk5G*UxVeJfz(yaM;4(;p<+!P!3$8mmk#Y!`D7 z1e-XUcVTOyEysYbM3OyvlsLk$hm(G_iChP=I!+}-fRuGT02(u}Anm`ruML4zz;{o< z9AH{XZBYtID4-XbYCbS0I2ljr!tIj|TRHi>9AgmN7z zuWbr_A}>Sngs%}lI7FNvt;okQaAT_3y;dt){!*M_TXFGuW0BJymerL!Q+$8DD$ z{6sIOE!Sv(!u5y-_0u2~!zC;5zo_Vw;&pU<4Vk>K^WTA+Jmf0CfQFX&yXACQ>d@h72gQM~p+zS}@1 zT;K!|AD1*Gi2Spv*x1M5@U5hnE(F5Y4h;C%rNa7YD{RGL1{NVLHGHK3Nd?)&K8VMn zTM4AXl)#nLNKE95epSz?(B6QmOK9|$#jxhUUS)8X4*0(dYQlgDBGlTDuRb9u&VPZj zA}7fvNG1TYKuo_OaB~)6R_=mN#QMD48w=^`?)b4DbMwBN{{R5le@JOQiA{6G z+XOORpuEoy>{-@6&YVzz;Orq#z5dA!PswR#wfv3z?ddx&v^B9<93N~pPXM7N=<(Ik z%&w6yAz9({5o5yFK9C82f>Qqg4^!k0P}*0o118m`TdEB`4Qc0!X+KX~^Ac#x6A?IL zlZ;y7@qHj|8dhKio9seJk9%050e$8;e09W64W-})ucEC?MI)8vBtk>k|#C5`+=DmhuPMj6P=)s=iE9{LcyTzk4&OQdQ5`32z<#?#dxMx2R|NQ?&Z z@c>HTUqzHcV@Dh$mz+I+(+Tr`0wFlWw()tn)E|$IYN0dX@N`PptMQl$VOt>HP%YPIfuqJn!1G~^NijP+z$br-|4e3BwsPSJVY>l_vN@8abjm#S6wP9GpY;%-ezO9MO0Uh-{I)6sF4xv=pK|Z@wddc2A3#JWxi? zJkUQ`s~rx(gE91}X_9q(ZlXh%TL(>-uQ;c60vZpsje!3E6EBn$s0#KY#!7vF9|*cI z!Do)D_9I%+*_nev_AXMLk|?3hnfH#%REVA4^~G)pVceb@k9nw>r%spgSXwB4H31WO zs<6%}v+A{4ojHd`=CG*`i_ zJd*8Ra`Y!CSL%7ciOprDX)YV{cm;z)yd)lV%$CfK0v^8u>=Wr{sXY)R7vSFmSU)#s z@uFe=rXj z0HzUzUX$lM#5t138L3f6YT+(U!mR+;j4#>1q7&uSGYi~k?)pG;g;dR`>0#H=cFVlDYEBK@8xQ0M3 zz`2gtS!7%Jxn!vG($+@jAoQC8<@`yBD8Gwi=ePR@rQ^*@hMR+zdjWcqbyQvXBbl){ zVukw?-Sl}4AMAa%kTcBc@XRj%08oa9Rf0Ju>M^E{5g9vPPO~X}VD*NgEO8yMds~SY zF@qDcLbn9E)iD160is^ajGKP}*5gYH+65l8sl}z0Rs)4)fM#8B*Mc9TjX!1(l)6Go zC{u8})VFHDaNBO@OW(VdZPBS<{H57?vdj&AyS z9Rs*kbkvw!=ky+V{KSVDd6F}4pd=rUig<}^pyz&~5vC~}{!kPc)L6U(ZW3R{vXuU@ zu53SDOW$wyHADAkHxW?3Dpv*CRJXxRWo~4%jnd|!fi1`*aW5l&A#pWMoIM4zBQE{y zH@Jh~N3i>=B+Qpx9)viAz{t*>JRnnSi=ccTn~vJ!JSC8*e2>ChS`A)0eF)O;u}oQ+ zNQ1#G;JRu^F;pU^UGs{#!Ok6l$9X>L!2LfWXyWNEr^aS*?u#S^z~BK~(Ho_gtOy5{ z_a&iE1d<(_BgXWOipV~Sfqq>-ECy4;^uu-k0DmdgnBZ?OCIjYt2tsU!QBwe0&)_W{ z7RdH6Sy>QQqACOzsC`Ke#BHqYDW%}2)9|yqSH+)4(GLSn5Q|3t0Q3gtuL+Q8$#C|2 z4Ug#m01AJD-sTx9JeuTu7&xV`R#hh$`%f{Ylco@$sS{~HWh% z_qpWg%$PofVAdpXD2PUCfa2WzAD@_M9s|n)GkpOi1zP~r3P~FDe@o0V}c#142i2Ks)-m#>)JT zANy5spB(WkSfTx+R~}5FLt?o7_ow$=7Tp_vu|J5i9}m+K>#ho6rKazDUMa7(lzv}l z@QxECKJQbe;Pf=GpFZnbot5;aQi^T|=F$T18X}88UCZZS%9np`CfS3qX*kX(0|-R0 zPJ|@0=!sn!xVjVo6oT92&v<1u@KnB0WYeF z{phK%qigsHStS{!W%^Clh0ydZ{`I^bipK7bXN3lLDE==w`vIcc!oD^K9#>Hr_QF@Y zicpfg5=FOO*VR6_LGN=le*XaSIsLHU=Rbswg3nzu8B1B~Iw#_OZVZ>0+e3G$S2ceC z+aEw+t@;2*V2>V&l!M3qKwK5FZ9Vv3kg0(vSGI}O!Cd>nN5WJ5{C;4RyT_d zhF_<++mqqolpQ%sLGsxkexa9)sCY9NLLWt|_=ab0`Z<|lAhrBb!zg&_AEd6BHieoY zHYou$klV(%f-2A9hQIPv_QoBxd3gZy zS-^Ob-oWFJO8N-k(n`VEbxC0RFGyDKi%E4sfV@j^^tPxSJ?(^=i2OAr_%Fr|b8UxA zrPyh7F^Y^T5DNS>5Wga6zCdume5JfTzk-qP{xx8fFN*{(UJ>D`fanoW9~0n#x|D7< z3Z{U(@mL4pz*!uAnU3mfn?@S?Hr^GytasBc&nl)6eAU$>1)?qF$)SQCD7diy0GFb2 zSbikZ<7&kP_>x2B>BS7J8_%emj5hntcs~ZrmcXY}R{AXNk{dIir~UDRx9{;yw?$^{ zImfKB&C;jHJfTXWnEP1!+jtd{tM<|`oN)910Q&wJKPT(%RkQuBNy+~J2ZySw(du6( zQE=yqtFWZ@;#n;a6-cT>6SVnAF;h@27|f8y$z+q#9%G>=da=F*HU)#LIHQqp5v@{F zRevDH6O?)~7tt^J+Hm9&v(&pvh}{ck_G*#e>M>D_x$YtnXv97}kciLX5)`aGMy)pf zA|tH^k3T%03Il^pJy85UZw11raf}`|W4QGDI|w#Pv{FIO`D;qIo)bSK)jhZ;($xb* zd+}F#uqkc94syV&rQv<$68NQoPILOrU%~J_5K_9n6pl*_E$63pO_S*4X<93?gI0{` zKH9|_SE!$ge-hfijwS2iudI=;%;S^)0RI5#RvM!Q^Y$`lUY_-W*9zCwt3kc_d@uK( znyb7yYC~CUT`xMiKXJuOCEW0L01olJW^d#dIdcj5H?F zNAkdrB?kRR#0tA9f%*jMd_FJYCkS~b0ciPohSLm1Kk%InhCub!s4u$#R^B|jh^%P% zzY!w&^@KAz5DXq$*s{5;AYBB#6$7IovVKYAg?Lq!a7n?hA+n`{mOj=`A3vGsmPKop z5#cX!S zg>mQ`1>??dNEh}{p`lh9bHran;!_$O@m-fT2lG}z43~w3_&I-PFB3f;NZy6`F zMfMbs?U7^!Uwcec{v?mu9RlFr!Tb<{w<>=t(x;IF<8GyY%HU}fehUG?vW8YkmoIB; zrED|1>C($&mrIN1@OhID<@yOCu3~6zm9jkYFK&GQ0D%)A$}y>MgG|1lE=!C3V7na* zCiu721H!l_z|vU}Fq^7;R~o(#lPL>ucsrpDI3-U3l4=cS2xTC$6*!2(Q&52XVhmV54zLXUaXz5{3_aWxyP6C z-GI6`+W8$d2REmSxV7|u8xb$Hl~NpwFeIcllKT~ph54O^bS=*4q6__0j=!i1A-G>? zYXkgA#1YL)VVXqs^;Wn~CD4mtn8@&s_+)wj%jhHbBG`3qn&3qmL*z0Cs!SavbtH+o zsRbK_Jqte}0I7ue@@aAtiQ-QRcoK02l(BCP==?dt_KKMnzG%CIVeiVfGWD8Ncf9rBj*-jtvUA&%wrMd^TIc*ktI=W)`@Jo?G zd3|QRFe`yB@I!@LVibJ^E>=gXJs~b9x6EZR!2ZV+$8k@1Kzfo!#_{_Zr}|R8HFI!b za383x`%qfOK))Y$lANTYbGn*%Pr~*S+4x#>>=w z#d^!P=@@SXV%r)RNe&Rezsg`s3?v#G%|vu-`cTk{nu72yI=Jtxdy2{ATCD}^2za&# zg+}Bnu}=lkYndvg@F>MzQgNEwb`u=Q4{Bl&zuC(~r$SkXHnt>FX`vNkG+p+HtFDGFd&TNk)1>q_|EID|cU&_my>p!mrSB_Upw zV~v4eTE!vgJOW`XP78)Kj-_Ko2mb(gX)#t;=&x#{u3Q$;lHoG9!nOkn?2%W6Am3hZ z{3V)Rh&=93kX=+fW#e>TTxY}Ztw?*FS@tcGQJ`z)g-x&UoHRU+uX8rOm&peQ8}b*p zXm*HnP!;E+w-YUWCYOt)KdzYQBmV$dd(QHXp}Z2~&f)kz-!UE50b>c_N|L$38MYnz z3&D64FegRyca`AC{DL-ux>UGr_yf1(K~XG&)j{P0^az~?5QGiqQ4K^cW_AM{?TavW z=!o)8D7mZxKB@u-LBo^jLCPc2cBdeSxWHy$mY2^*1JZ#{VIb3A0bLgzq@}B&TvNN$)RKDJVoBY2Jk{dNBl8F_749$` zP7f`GMU=J>-JTY)aLIZAybD5~3O@zd*Ltrp<)4|$vbMgQ4&tU}I|W?|iSc(ALY}S6 z^&Ju-$YgJ6m8IfXZ_GHtyt)N&Q+`!&s%U)%1@U-aC!=8c%ZR(}Bt66UQeLZkPdlsW zxZIRD9p`~X@)I6lrr_A`WFZd?WcU=ll1yaW%@gw%ls+NyZcsl0ZC{k!uN-SZ~QX zMVvys@Mm%cDP*js>`L`S)rU+K;MW8gQr0<&LB*t?k5mc49VNh1m{VwQPK9Y(hO6qr zj_fa(7UFBRDJDA>7YUP>2gK6y=v4{&9u_BV0%VC^6v(HzVQM@1I57nSPFRP8E$cx2)ibCsH@KgJ+ zmwq2v`Li#}=m`0`XM>NpV~9@;%;tk*G&#`pbwm?jq%G*kUeQ+qvC3#19}xO6N$9I)YwhH^4yTBSgFLEgQkm-35>!`g06}gay zPj_gPdWiTZliHaLanyu=QC%nKnpW5@;>dTTI`_7NGd>RR{{W|OE?QGk)c*jaPvmsR zxj&!y_ak)|4xwfmXK&1*Z3&qYzMgq62X%5AP9#C!LylnaEEPDXw!Zfhf>((|RfCH7 zZMlaj2rKf|9KG*K-BsZRmBp=^;Z7`__LExQK{thZq>M1x{t5{F=r^YQaEQD$f6NQX zg{ZjpR}7fE?a6|{brKN0_}84sJEwdP8`B4+xYD~oKK=tU<>Ba1peN$}PY^dvQCmvn`F2?8E(%zjuyr69au9hmVye`wl04{2Kge#057 zi7nt9Nh~fmQOHf{7;nUZH!AU~>28~x2HFp{$a^W2-RHo*_t8##9|9984eJj2&i+BP z;}E9E==}++UMIO+_gAgt>US8qg=gE+RR!g$A4znt1naEKEiXaWTqG^wKSX?&`Mi|F zEDd*MlKmFJ`81_O;JRe*Io&1YQ2M=Estcm@3$&~p%r5M%sSCYy`MaD$pBayr6?LB! z(C-HE+&C51-_6^{7IBx4JD$3_gsfC#h}X@Uv=rx2>qpn&SuU1qI+#|N78ji*=qhu_ z6c6ebiHn^{5AYp|{cNtjfW8goemL$SC6F%K<`ouJI=+kg zb8i$8VhtMKq~BoI%uX%T0d{#E_$DqfQtL9v(=2=5)qR;gCLS>W^W9Jw_ozWmrK2GD zK>2+t3hgV~x$H-nG>3%v;@@ljS|i|3^s5ixpTd_SvXqSpT(FLjy+{-Bs4L@CdW*VeLH3Jy z9cf0oqIhosdGS}Xo1@30Laq{HLY!khm9F{QP-ZeMEn z(AMcHKX91^!{C|h%l&fxpwN~ou)Y_PA_8wX8tr@En$>I=>N%$%y zw}w0!7I2dcB93AB?qtx1F$*vOj45Qal3Z(`X?PjU@8&HZV6&1Eku)~b}t==3&GvRwfS9>at#iJ^zert9De>^ z?PV;x0ZxWdmZB6nhsCkZjY>(4Oz8P9KrhLB2T!DGWqTlB90I#fkKt~{ZYrS<`@ksD9Z2cZoZ)s@Y{(3` zQj&cgn!^ue8T|pcRbl)>$w$bxAHn$clEE*lhrRa!LBuyH3)3&Sg>C-#3n}D{- zG`>jCR}sk$#|v@r{7eJ|g9-qhMos|bXOGTsTo{Ye9B?ZzYKF!>nxn zp{G2)dJ{$vypb@e;Bm*!D6!&U^Wgsg0$3tJFmS#G`DGUH{{TCY=2)WPy`wQ*Bn#^$ z3&q9jb8JFKA;tdyBz}Xx4fOt6S9nKvq^h(x)=pq4gJL?w9}fCgv5~NSiv26Q<9!_B z;w30swLw5%(xYzfrPe8iFtXAmkE$@>KnKSnd5`Gm*2$30 z8k{PIIp@|}(IaJ7hD;8CK4LO2^nBW|A5&pS7yVL9z$}ogs-uhYDT~mT_!wQ`9;cXw zX&e)Hen~%ToRPrFH=6p2@)gk*sSuK`#yXfCoRI`z=(As zFNG}T;AmZq3^%o9;5MI%14&aaIj$dAfYhyP3R>w|fNZ>LECI2B)>O+52R+~>Fy5O? z#AE60XDtSBJ=itgayMwt{Jc>t#W{<}6uhze8J7V8ns6-tB>&<)t;Iuv${2 zHH`Q)W~EqAz`?tdlDZGpHpfB0?6kVucrzOtR=t6Pm|kEShCx z#IThBV=khWwSd+SHzl7yc9kgbG;45n0DY{1taEM);H#2b!!@cPJH7NS#|;X?7~r6h zgVh5c3`JhR8+O?OFvu8Ey45Pjv@%<#4>thaQW=bBEh0K?#4;INz+uWUxRM4Iw*d5( zr#+yTSm^~6(*cdvWv&|#D+~Z+NiA3g zIc?wtVc-;>fU|r9OTnO4Z$XdHbfJ;s&`K8oT3&}hEk1$qyaAS265RA9Ak0_}LAKx< zG%r%v4(Y**lPm&~XdR#EgH4BlEx@fj0pnVZeb@)KTJ!4z6=i5`kl+EUi>zs_3}(DF zCY6Y!k3rjBgrsSptb*I1x+b+|1dj?;E94Uly} zC4Fl1h9$J1V`fzVYS5wu>oulSo)Y-#xD1-_fnkO$mV=(#V8?igfB5(YlNscpt$Cp$ zwlcT|w>r=o3}pb1bX=jT8*D2Kx$r6s-I-bgmFg0~$F{C$8+$N@hBI#Tje-82C`;=J zfH2pM(Vt*#;zp~IrL?70p?Y~N=Gs9CMwxWGZ_>>UCS1u(&rNRhYOTjhT(L^xP>LbEta{| zh+WLo08GI^vdLKSl&wUzCMCcbs0Dz_Ad-=pn_-^_n1&YFXl@M=Y?w^d%$C8klhPrq~ zE#_5cUME+mD0FCmOPW%_rVATO5vXCz0#<4?i!yE^uApXllwHQb;&TG=F$8fNI>f9) zh+S3AV7C;t?g;@FW+38SX_i(c(Q_9n-O6(U%9Nn7Y`CE@bdhc)N^f&)HDd}y0#g>8 z#H0QYn4%m@MH`DyRxKa#QUy$#+@Ux<(Q_fe1;Vn*&HBnr+yTU*0A4jX^43?Xc&DkA1; zX5nx~dP}rXDqUg)q_Ra@l(;phUL(zx!r&N_baha7R=tbPd}ADz^prm6&QMlj;?tfY zif#xEO(w1*NR0dTE0OGKz6AMtY>vdK{;5L>&7U6l*sUs5bBsm`H+ znPtpqEB;uHRZ}$^NFc;2a}+lRSx-VZZ4M5X(iw2P#y|!`botQ*U^-o7*AN62t_2tv zmqgUM8kV<0dP7F+&~HD;NWA#|B`G%*6s*9K!x7v%)Ea!FTtzy}xM1c1YP=y;FdSTl_mfPuShLhEx3TVT3yYkYEdp_iHnW55awwWt7nQ(xTOXLxMIg9 zG`Z#gZF1vhd=Lwr)0a*4i-qxzjP-`qZ;u%5ABapOXfC5_k))`V2p-)2C293wnb$~H zwgpvPVXZK1xnSXo(poQsRftaGqjltE$CUF;-gE)bhTE&=K1j#Vz zmME4Yw_HjBZDTM+(F~R_%3dI@b8}BfEJFgLhEavwt7_o4H3|~Jmu-K>n2T{$pp{g? z&LXN*4H09L5G!6WPN1u&+^}og5e<^s*aG-*j@Xr)^HA(M{J_XWj|+$jguoogrMoMv z1%hM>m{J1`Lb7fTi^Z|Xm!XdOl9cH@aZ;!upQtDZEGjn=^jtxU}vhf7e zFsL;Y(H+6G$p!N$u)9+jHp~tLX`HtQTZ{e6qW8~aY#P2e{KQa3mSF>Ptiu6E5EdO( z8KZetIHA}5f|V8GG@dL%%YaoxWe{Qn2N94$qB=z_fFfuT;&4S2yw;$F?qeb)Q)rcA zVllc@Zt({766R9aEn+TZw<}VyEDB}S5Cd?xEGpT_0~HpMxt19i)+*NXvbU|n$`>je+YA)k<3%rucj{IWYqS%bcLawF58B&xKjpZS*y8%vlfiYXa?n>m{Kb3 zP)ey`xZP$e80i|fF@=d>g4tO-{9qbqP|E;9G#z(|lyXHr4|t+3$zW@wV=le-y3%sDo@m^s3H&;<3j_QRrq+ zUHuW3UZtXgL=d5xyJ7{~p79vMx8Ju}a*EWWrcuNKKn7#t zmWnY97lU{%832}Z0;!jIg9ATu$i1!!V~#6`ZH6~6%taIu3d{;qErKIN0?>-6(5$g; zP(Bc=C5wr8rHz+zs~qknVgCSTqgD|dUS?}iR^=jcmP(6|WtEr!q7=x2Y5xFYvcO2^ z;yI7RJssR8r{&`Gi6@Bq#14!Rk+xB$et(jtxY2iNKBf{zdq8gcez>3#c z3yj4S$D}!YzC048_VMWxfXsh|%t{KcsMZ9*f>DR3$L64dZ3x540scL8xmB|QjM+ys zDYL}DBGw);I$7@!4mpN0fJ`#*M;%~UAAhI}#$b!Q%ba2rM&dzo^5^jYSb5Co8Z>h# zBaN*=wjOZ7brEPTa@T%w#1fvSt0r9gAb~`v)CsehKtphX+*Y9GzF-2>8>pue(Wq&} zbXMVIRv;aJB}_~6QIbO8G2;|j%+IPvh9w-j%RS)OF6GLgY|m^_egfXgrJZyWqd6}!-vPZ-p@OE6dOtX}3Ko!U8zm~X#% zRQ~6Z#jrYiLADr(&ImP#NlhK*QxIR=Hw{Ds<1>RjVmF(2EJp#{1xqZcu288)u2oYQ zWA=eymtZg%U=P@tB*p|CD&6jILT4(!k)#nQBENswRF^8z3pgNFpn@A>5G2PvAwwC8 zrA*XO<1DKniV;y28ji_!TOMt30f3fOYFSYwl`ioDU8J{$W=9@yOI7enwF3O3MIifr zqAY{jA#6wWet(&Wt@umEk*;+b1;m8-ssN@Xj~)7a!O2l*5A0mW`j?!a#LH&lH^fgU zn1Bb~-`p2trfvk~(pDELuzW?lGV9J-1Jn01J9?q-39_%s{{T}Dq1PXYPR1aj#*M&= zv3$TOih!u&Ey9KsOB67NIfpO^;s@GLPpntG#jF;}P$4*X3#Q_ZVP@mO^%d(HtmC~! z>VPG(MCS}_+lh7D#aUpOg5s<+0yJDkCF-{~DXEQ^u*Z}a8^`QZF|e%gA?5b zWoAaVH^aGybp)7;wg-}qx@8vPgwYyQH5%8UneL^bjrY=Evz9b;c$Tz5`Vdo3yrC6W zY_KYxV-69yd4ix%2kLbAL(PclVe|dK5z(JmgOW0!Bx`rB2uQ|9NkN;3Qrd-WU-5L@ zt19J+SyPrG;7jo7xOwIjB?YOb>L$aOfv&Lw;72RHKtiQzQbaBo9N&CYuP)*x2<@rE2fQh|AcMkQc@j1Wi{0sx6_asL1Xt@APhp;~+R zl#M>{Q&pe%9eT4a$zu>WG1I?Tq4$NWdrA&9-?XGPDp`qo9eU^HWyBZ(SLOJcZ^~T* z)=|8~01o`(mXrpCR7!yL&+pJQpMZW9q`SSIEtPZ=(HQdpx9ZHKXUkz7&Qw7Jz z+GK-2ADKx`{(Ao9g9WHB6j*vo%2cHcq{#xiAha282pl?I6Pojv2Qrz(9DWGBW5L^+ftR7v|qfUdBh9^rDhFRUkGFuL}=Uu zv=P)&(1_dt%qs9(FbY3WXaWUzLd671%mA6aKQhX!OdfC!^BO4TQKg3brGhYPD-_!h zNVSL#Ie;;Fv=P)tc-MSP7Il_WF==5i8J4Uur3NUs)&iM>N?~&DT`DYXmYCL~3(ix+ zwpk*y*810XoWwFxzdjuDYAJi)kDlI`%((t_eKGrtYhK5f@AVL6Sl{2aqPD9HM=qIz zcXwLL)I7QIfNLgCSZD)g`g8cW+FtGZ^8QD&=svlD%MeBkIfAO@1t+|@PGMKsjR4(w z{{UgN%cQ|mG&z;JQpyMfu`6*~=2S?nB~2rXZeWWZ(X%B>Asa)^Q)lirg~l376L4C_ zqL)w-(}_y49(M&J)kiIN^HR~R#X3OVPV&cl^_Jds1-48MAX@Pjl(i8}`9j+=+G-(X z4P3BmyNDoSxs9^(0X*O~(F9#VOED}2wA&S!g}X^`&Spi*K^34g`1OD<4POc5>vjHV zwX^tDSI@*jmc#ht`zYNNRprNz`vg_x!PA)PULE?>PFDAR9X~RlrFrHZCi?n$SB$NW zAEnwB3YJTC#rYLXm_=o}Hh$ER}g@Og3US9pC z4}>y!Lf0FVR70sPe@Lx3mUpQ`2?qQ!k3%e8C4X#xruDq&utY24%R59Q;AT zSG=vYsgP@4Q`AP_vG)WU%m5K*%Y8Ee!C!GQl~z2y4!&`%$F0s0)a&i>DX{oK zn?4=;L73-{IM|C3YHp(ey2XAW)G-tS-+!2a+tLua=D7UBHI?{FHlIJw7#33%0$nA2 z63SCB045jKQMxDBSg>{ND+sSRC1@b^y}xlbn|prZVOYN3Y{Hf0nM+1`Lo38?Lv_D7 ztqG+5{GnlU-w}Yy&ic%Bgij6`TVF@@1g-eYrGb8RDdaq3N-}KeJV+qcwV6lZYvG2y zxG1$5JR)6Q@2`KT zU5GyS=GQ&cOLe&ISPnWJutzD*rr zshjbvN>|69_A|L(5VN7`c~)RR`pH`XRIhexhp1;|haN)i@`bAE?e5cv~{-s-N;t1*9;>7g5xVJNEcN{POQmzKpI-TvRh#;$gVCa?$7)@nLB<#L<#2Xk=wo?dO3RyiFFp85iMxxrse+O(u(i(< zW>dxpik};ZK%mc@$B;wY_wg(mJ~7kv#Nayc>Eo$KDmeZ7N-RBCT=;L|B_j3Se4{Nh%)XnvRh{RS zGSfZt2THcM;tSl}{%%pHe0R_8$Oh7M^yBfwrB@z#$Bfbg3-9UDQA3F2X}as>7HRLt z^Q=aCzCCYV~0j-FbWJRLvA8dQD!;Oj4*-wCCK*M-FeM{rW>J@l3cjf+jk zi1Q771XZ!ug)s_owac76p81GSs~2WBq9Bg^;LG7M3H5u-xWheo`iLd!eC7+gh8N~7 zaeU&ATy&|3LtTHQpxf*F4c2re(5K?>aDv}CmaHk~n~hM;Z)7kT@_r*Zh2A#>r;g(3 zrOo)ot=CvU8#+X$tUM)$S!l~1U+fmGSA8HrX~(QUg=;o?Krb&pXaQ?HaqkJWtKu#e zBQ>z|{Y=7FnjVdgm3jO{l|FYUgI)LM9HS5`RQWPo#ccQeLBu!p7UUY@`~1W-!kF+l zy?-Fcb?q#!S?B8;^MYI7%2*5b+-a63#{U44z~8Po-<(9w`}vDg`1T`av^Rbd!+JaR z^Xmt1obpP5a_?~9=XvL{6(VwuZt)vZ@q`OrG5F~YrKR_&ooZMsm&4{aDSsZ4 z=%t>}b18iEB3D)2?ii-nmUUIXIcCqG`;M`73*rw+lJnl8B|!kL{UK;gzzaO!1{L}D zlwKPBz{uP>@|f&tHNKnm)Bvs5zuW*Y>#|dM z$AlRZe4Kx^5oezKCAWORihUr^3%hkNQxLJZ3@=@EI_wbgy|vTUAxgi@4GMJn@8Vg_ zI%8frU57Al`T0z{U42Ht zvor!>j{g92SntjVVFpz_Vp|WSpd+5~04otQa8-8lfHZ5b?iM&;bzJm_3aAQQYA7u4 ztXQ}ED7OeFTmHb>vk@xS=M9$X0Z#t_a3B?W`Aas4T{xBtuKps5xC2Ia5!-MEjX?!p zoNJe*#Z|EM{6oU1KiP=3suzq51k3Ga<_)BI7hP`jB+P>1R8u0LlEkhb_ zuH}#JZBp~br8tDK9FpeSDTd?myq<0QqcTG#3GnT3dh&>6vDtmMj(qn zJ^M>gY89aV^neg!!cx%YW=I<0-s7r$S+XE?iDO8?t;9Syc{4lK^oV48#yl^t#Htnc z@Rc=}uBQ!Wx8^5dUvmbOTMr%*s463O#HA?jr+zR81OsN}4Ju#6w5Rxr4o82uPDCW$ zi9>KgH5)Sv38UYHQKz0xopUm>^=IlaAr4>w+W7wEMmUPM91z$V8;stQHF9&(D309t zKn{-a`jofXH7NAv2&%ZcWgI}nCdd)5f%uYrnDA%k3 z+wChEJz#9o78`=9JJw?eT)G#h=44bA%qe()*h1htrxOz_t1K^L_Dh&`EUov7-4`n~ z_QXM=yk^RG_?Hq^Wh^l%*CipsUI;ZS;&DUX$xp8xdn)xou^^ zsc;V{#8Nrzta-lUmw;WG&|x4RvfJg zV?3W^5TfS9kATErj#y?POO^O4TNkI)0v7o2KXI%V8NGV`r5h^2-x7gINkb4qiJ@-@ zX9s!y%-z?lrKo08Ys3}SUulzd17>UDWuf}x%4aL-3J%BaP`x!dGa9VQrJj+(j~@IY z70>H3_;06vGNCPF-@Fj#dYLl?6zGTw>_l6J>-iSCE*1tM3)jO_8hFOSRt6l^dBo!6 z@89AtT)}Rc$giWPuP-xX16RZXjv)Ypr4XiahRQ28;-C(G4_MnbpJI=7^@Cgi#}A}p zVILZTt{U($*Tw>V6BNgJiOB|{aC%1>%+<@;r{?3LyQ_(xJLN( zjTvLqN^YbchlDEJ<_KGu=q7)%u3TIQE?M2`FDS+vdP7RU;DD2le;C!W^p2LwAk4_m z1SNZK%hoh&gK$bHmQI8#Jh4kqY~_s(dDkBE6_|@X{Y}t(e;zOs)xnubYPIJHJ9U6r z!|5#bS+}Q`e-Ifs@qt{uXEZ(dc*Q3uQWQyr&Ag_h88u7j@#xA^bKiuiKV$ioxG%5r zV3;%xG2b6dIv5~#sKLb|P!U*g{!OF}S$DWa)cEE%TE0`2j(M)JNUHw;J^uiaz_Hf< z0FY}hUl~KuFQNb)yYPet1#iM-#lA38Lp~4+dN2X3u&S&;7$S{R7j@CU=@ev;Tzf?p z&3ADT(Kr=F!KqCP2o!M>9FqP1Vb#Rhy6?^i6!*+P@whIl^(NG#$CayeG;X1fhQ1ZUu@ZNGX~~Owgj&<#TMUsi~{4StfQ@drJZ4JLjM4h zfvIQ)?j-{m9QaBMo9?$c$A-P(N(PcujU9OPnA3?sJmPq3h>HT7g>|e)R%YDzdE@3{ z5%u*@31v!S-^{ZmonU3@0uQuo-Fw_XYCNIp*g~S_6E*hJKC_A`Wf+&K`vI!YSVN7VxFhET} zM!(+JSx0~Dp`xn0{z8Rx)xv@tekNNwWz~+l&VG4?(_ay&9cBx}RA{V9&R4eAztM8ap8`LFpOMZ`nA#g^4FTa_5!foZF-=B!5DO9zbOA6~Q z!t3=YA`>LTgc_C@3L2<_+br$VxVE?Z znofW6PE8RVM|FxoSQC-iM$bFVkQSk=}d`UC-m6n)AU5W}eeqo?sG zJYrM2^nq+@4A>%O2yG0&w^dy8iE)cGOf7Wx#28xKqQhUP6~X+MOK=fanL&mW3jntF z>cKkV1`$epTwi2~OEVUholQj)rT-8o2RInv0ksPnT%(S!8Dr+9G!*wu9d&VNKhkQx}*>DwV;ccQLso#WF z++)uj;tH66ltwp~B%m3&uD2S&N$nb28^l*dKve5qxakWb{>(ILULJA6y2KpKBBTBU zw^jR%0Uk$v^nxBKke0`ncj*B7vGGJy27P}qn;}u){!5jOI%ht!H3+^WazZS<6v3-} ze2}2@m&e?G_7l{rbyY82kRjZp=P2M1re`3NU@eYfPRJz$%$My7hrWO7Zi9 zv`+lALi@J`%XotZ?~4_(0A;Q{pz#Zt@I3AI>lqnqnmZNO7yFXv#EyCtrQ|l8>Ak~?i z4L}nb5frG<#U&YvtxD1!aiIi_JWMJPy#w(R)1I`uLQfV%6 z5}}^2`7)sp-eK0wU);LXs6oa^9ExNb3~)d}7gmp7`6mZZ?ucv;NV6MuOB$iXzRdTy zP@O9JgSDAt$-bYt#y+C0`5^85l7|GkVJpnkv9Va>f}1+wQnW;j@5FiAxfhzCIaaL+fMeTi}`l+>euWw0= z+9b8_)-cK23y5MW1Kw&{=^Jn&rp!y-BHg^gnI(eppsgT26@6DzM7jGXt(rLFJtd~pzk{{X%IW(Di>5OpDX zTuk0&6@`91U@5cxn5rTV(RTo#m-i5P)yyGN8ug5=EOlM{MpT|N0CbLROxQJ0@yaDI z@=%>MjJ&(luu#Ovl&&SL`${fYmt$@tr2|CMM5|@jfhf2tWJ=-}bv+{8lLHJDr7%@) zWjlA67Te8!{{S%}jS|)@WMx}C`+H(2xjXzy4Y}9E4U1oB2G%_DXI~h&?97o}PLQ{D zs8D3`gfU-)bW8FeMqx@&W<;|RqV8Q;)I_B^%<*Nt9@Bzh`S+J9^A#pn8nFwg67JE3 zU%8m91=V6#2ETv4v29O%B?|lyLuQx_K((nt!~K1}CN1A=6>~DK_1~1SN32rTy7%Sd z958k;g0)_qpHqBy?9Ao|%)s7v)R=5YhQ=UX&;was^LiPSX-f#|`1Y4!lgs-JT!VTw zoJBSl&+aQ`2+)=-n~DS*Loowma{z5%0EjB!<1w%!RWpOE%1gH}ZhkxPj@F($%Qoc! z7Owf3J;bGzj*-LxbMNcrA1Gl(fu8>WH!EAi-QPZN^51;_03w{X#6sEMU&K``&pH19 zX03RQo0t`1qf;xR2D(E^p?&i^r}kRy_NN&`9gr6hNl9a=iDnq#eI>InYt{m=?j>8S-!W|z$ixz{ zEl?FqI+)a9WEQqAHN3c$Y2Kk?t}x$}8WyG~@cc2V*T3X!w^6c)3=aNDYilz#ZGGX0 zYVPyf2T!ELdOu_xzzsyrb(v(r7s_TLuMl>rv9@#&bww)?WA!Y^%iYbYN;1JtNc?$u zU6RUrKK}p`!y%rrD+cCP<{(oF3T5mw1ym4{qLzl@l~qcCLgHqfcl9U~V2zpnOpNgX zYsMgtXCH}M%KY`$9`Q6a>-g_bU7h?usw=P8&ZUZ0d*)abd6X@8*(o7KIo2}(FJCcJ zPfLv``Oblrvnf%n!R^4gg)FR87Kq~#mwKF%+DIitUU-LME)q~GAxyxxzfTC&0QRo& z0V}uH+^pxAXm85k6-;=3VBOXJpg?jN89UyPE5c}g|*#gRzZsBc{ zCP*YEZxV(Cr6M*<2(-D(xEf(%pqC6Y#B8Qlh;f$?L(9j%+)cON`vAnPD0AgeEp5bG z9KaI}J|z@Zh?{d2lNBs`{nQtZem{^0L*2{mpX9W{19;mDHQsJtNV_ik_>E}aoTUV@ zf&Tz{MW&!pZsBUKG*nbl{35tvs5jv@Q4Ub_hX%|#6Bhz^264EZ@fxa)kWDcLu?Jdn z3b7e<6~}(E=__)oqTeW5^Vh!!!A?8OqBi%4lZl?3p7OAHIgL5dEGdc%MHK9WtijKI zvxp5;O$H^sOUetC7g+L_785q03=uaZ67-bY;tjJiJm4l~Mxl9a#tHV82UrIjN>}bt z!#(%mEd_a)68VZu?}+nm6e%kI020XN*@GTAg3-D>T<>m+b!FCARg4ObWu(r4jd)nkzAf zc!w?CjFNTf@9cibgnyx*PC?hW_Cek$C1Cf?P_*rIzItl9icPke1eEox~s%ODf@P zOPHhpDo_^86E_hofB@~B!v6rjxYu_0Bl>nE2F?+rAj$h~V#t55@0DL*`q|XcAv<2}Y4s-Z| zmVbmYiL<`>f+^e=W2eVhQc&;G5K4;s@RmGuimD}&_u&Bq46^vT=2$8F_kMBLKXE0Y2S5qdX<&gw{ZY*Ve{$Wn4+_iwmdxF}}>$W(rmVF>?D6r=p^^`*D z{{Xcux=ZlHTwTjuu75sK*1nMmY5ekv^~P#it`G`w;{dDm-@Lam@BD#7xs_g^y^K## zjH)qH|HJ?$5di@K0RaF30tN#C0RR910096IAu&M^QDJd`p|S9h!O`LI|Jncu0RsU6 zKM>gnI?Dm+EFsSj;=&S{F34|>p}THQXP0CP#`yYnUyEmcZv4jWp*6lzTogUrHzAoApmySYhwziw}7bk2(WkMahyoGH@ zA3+VX@cJolZwn3jKJ0CmWyU?eVRPuBT>5g+Z(pY}@9B0uvHN{KU=$B*swHD&Hv+=b3V*u!aKV$)|M0)4kE3*+g(YcaRN zPFz?ymLV!VxD%dB_|6u-VRFLt%MuVH1;L00~N8S2K_+a&1^KAq{eFol%3 z*d*nWI?8PyKz1Lm^m=6TrH@;`5DZzz=$Rt2iy-j2eiOtAcIHAl zY_Hzy^u#rtySHxI$id9BoU@-o54Ru$oM+#oTf{o$hTG4gW$}F2j^5q+rd+=%Fu9#9 zy||n77zX&;*XfyKzf9el;eP6sA<-N|mSWcD)$C0p@nv#Dn`yc9`RQ?Ikg@E6=hMGK zZte4g?1cH3Hcozaa_zP&^fWR$ZS(X1Y#(PJ?!(Iv$v5atgBdsIE%FxZ=E0YXA^E;+ zKHoxP^ykc&+z2}-ApAkFZPPO3u+{}^3?xC2h5*FRc1j;WSTYXU-TD_yghd6}z?~Z# zutx9G=VMn(;sJFmTZv`ibL;DUpGRc2#se%T%fA;-i|*ex6UXS{_vi*lS@(+olJDMp zu#~nVZqJuZ^LOZj2P8&DxsZ=yIzi_?kPZ`kGv@EoI)L(EBxV*2`2_v`iKGtl9vH#K zZazwuHU%$E$gpqKmZy=Qf1wQDrh?OBk7LWS1L~Jc9J8|+{R6fzKTK8v1L@Cfj+5o_ z^vM^`)8l7EnEe%TZS9QqCx?FVUdI|a7o=#yK3lAi>v)j95W~xO$@^?Ys7JG&`+AdX zzzov-aN&~ge6;>#s0nt!ydX1>86l8;zK;-OWsUkd1e*p)teZG<=!#xDV=R0@u;J~zAO;@M|UB+63Owl&iz6TNL%F0wD@I#f(EbJEX11Q zEU=_K$VcjY-OTzEZy!Km^d#k_hQS;R2c5efT>IV6r_5$3u9v}M)=tT zm%c|D$iCk*xp8d1Bk_X?)yW$P9g}3+$YH`m=)>%- zYV0%QkHw}3-=m&=1GYP3t$}3Emp<3T_voU_@GUCUCw|vk;`xA_-a+=CKMOJi?5i7=KN6fQdh_!3IfQb&-bav~=ONt*;EU9zAhFD_ z9JF>~K<=n-P{(;B>OB@LNUrh%WVmO{uG)~4S<{iDV_eBK#=!g_F4*im*jc_w89pFS zU^-Yf2idMSkuQ+AP0~fNCrNp`c!ZzFX44SemWt>|3EPFfvCrvb--#*nZSw@&HToSb z5_K`Y{DK>hMax-v^7}71L3Erd&CYMaf8ge4%BpD6c3)#%}K>Gr*ip)qcJGjbxfolWz zw7;D5$YaJ(yJNBrHXk#&zSvW-rfhx2U2pQskEu z?W3!FCdt$X&#;Ziycxmqsec->&Q!henQ9p08iQ{_RD0?NOiI@(b9Fh zv9B+~Z^Fsa5?rA!{gKHTD&9&%x%hnnUu-Xs7ho0v>;oX1bB(Wv8<#dLhu8_Qbs%hx zSX?p&z_54d{A}(?v7UZchS;%@x9w*(!ucjNMw~B|0Qj+Y>1XNzyB~Ho_8~kYJ(Lm& z0NY?HN04n*r|59G7Hc+>z-20Gsh!2jz{eM~<@o@~%eVV|^oQvg8wl1F+p;9>92Qvqj)r znU#IpyCLk4V0L-$(+DrYus389w?FUwpTWs@%K@F8S8f>Dx%gia`H=a!^8ue!Y+~w1 zK8(T#EaL)PY~PjPxhOV0fw&xvE=cFza>J$bdhD3k*I+&)J|zr-^~uKk)(5g9WHYh^ zg1*24ZH8dM_hUzO8a~gNulTn6%b>e%H#t8grOR!aNc=)ZO97FZ#$~p@dxtXI>@0Ae zx<&?t)wV}B{Yl>6g>Q>V>uoJBnZSo-Gq((2GCYTRL)3(2Hbs^1&^v3g>!c!iU^Z}l zg!#TLeuS~GzAbiA*w?n9l7qJr=s~VWn|E?tG`p~gd}ziS*ge=&^0f}0`C=OwIR-%s zRu%xX`E?|amM?Myn+`y&r~%01a>G2H_VcU`)&~2trtRI6nPc-GD_hw2{zA4s5$tS= z*`}qhh;|NaubbA_=VvaEcFpvWICjBg%VB1o*jVIcg$QyYAuz}kyd`Xe)Iu>?7W9+!xJ}i)3fm_D6 z9lj)kNSH%YX9j)j29`25%zF-Sj+Pl|+c)L_;qhmA0|w^i-A0%Ph4E-ZGh^^e*) z>^qV^FIZV?J+-_p?WEkga=~jd)T_sDS2EV!d~AnudfQtfm+3in4nf(+vzyBYh~Jll zY3G*Hx693p&chsGKEcj?GHr-SX}U5zg9IoKz4*3$<+m)y=pE!3NUUOZ<->$;)r{@r z58eY0h%|lrLrkaHM{;)=LUYS!18k-y+HmsHCd%B|CpGF6K5*Rd;TZWJ=09te;?FtU3F}0Q&&-xEX8ZaMAvM<%;g1 z@4bOOU^cKa%x(CLhBl36M~wL)(=WpC43YLu8Ohj%9C>#yVEfOIJ|~5ZlPxyl@6m|_ zAG{CTA9!D#^I~V{4+KfomyBe#%IAUXd4ZJzei#Qy*?e45jZzmnKnO!@sJ2e%KOdm1jzxLP~^ z0LcsS{x1Ij`u_mgH=VHb*bm3~fYs{ZGR! zXJ7VI-0ZWcU&%0H&O}zB+boFu*Jp2w4UK+2oX$Y7^7}5v<>}U25=o~20CENSBd;8p zYeNWkcF?&cAn&rV`3O3GmN1-kLUfs3jIE3Vf)0aoy0_` zX{_J*3JA}=l+2L$cI?=}-Tf?yt-Acb7FU8m{R9qnG2zL>zr3CD*z;sfAL7y8A8(l+ zW91+hEc~(zbB>P950ocE_h7s*DldSr(^4$0Ls0*esltU}po! z%$Jf$hTfUY;zkjcmhc0@-#XQkBlk0I0OKmi%savZXgpia!!c5@4-Cp;hD-WNA=+3X*a zuqQqv&OkoOISSAR=PpMN(2N27COhzp@X{sMCRhjFMe-<_^D22Qd)rUH?{7#4%VIXy zw?pFZQD2R*j#|u-G7xz@Wv zhJLK!-HqM1UHWzWAkx;YvQqmnenarMI`3P|3-93w^M#M5=iw+mpR^s}cVr)9zveF* zUt5sB+yoW}-r7gG0>G*q__t)--GJ@8x#KlVdsdlkgx$#(6rtwexFhE%>^zjGJJd z{T+eLYi-)cX=b8UpEJln|E!epS95kW9q|lATrnU1Nu(y&zJsO3ibms z2!3s#1L5N(qCpSz_QYU(fA%#5{6{~*B3w_Nzx}tmujeR&E8{(fXVwwK2r=ZE%a73G zr_a+dUUCbkTg9!Gus>R9y3#TU82ByK0qjW>anaI=dfaAZyb0^tR z!e2RQ`2FW=9GsVDjN6;SRByLa*-UO+kFjiUl5R*QW%XGqg zZ0Ei~K=bDB9-rNoogxGV!XRWBhwo&J=fl(Zmkh&of2j+jwzEDzoQZFQ3k*Ts&}|9i zwteg}?}z7ZeQo46)#EVPqe#WRVVFZ+!s5XJIfj3KIn_Dq{rH+EAbRuT^ZsO4JN_(# zbfDc1-XN|+n8Uh$vbee{@q8ARb?k_l!1{FExS1|mCYLuR>vTCD@?Nu%1IGBs_8*fj zJ+RKe-MA8#n`m*#hPfbQwS#BvydjL{NC{(dkE3`V$P!ca9^}P{WMH< zMBTcCzElp%Y8mIk#qPqq`USE_vfegyCwA;^ZR$zq#hg!=dpF#B2AWA$n`@)^@8x(8 ze_3#besFuq9eD!k*H8yr1M?tEIDaw^QNc+m-rRo{o6_D(-hEZuTN@m@kUfN5ok{94 z6lWU}A+U|>whNmAm+5uV(OW+8VLn{MXK`+&u8WA095O3?Bd$J=SV5lOf%b?O<(!}3 zm|TM}`U1HwShq|M^DGBtAUuQxlUl-FdwgH;o&0~lX+Y$?o_1=I*WBcxr}O;bX>j}B z)BgZ&%UHkPK5i& z&9s&;BKGz@4n-ANUW-?4Tk^E=g87Hy#nQ;^2z}=UPTJd$`Gwng$qz35tP__9O3zw`CAM~Cw`J`fAp{w917?QwsLc;}7y zwugS2>ehPwB8A1#7`Y~vmSf9O1IS&6kx7dp2h0PALO>W&8^fQW{qlZJKbQMR>395H ziOPIM3K2H+KfRYx)t|4*KzSEX1t%nd?cBANKQJM3K=%F$WML!Z8B=~G4*WS{`+k<4 z9C;w`-#KA%v_K>8Cd!8o!| zaYl~8@iunT*UXvP1Ov8=AMgfCqIc|f_`8ce612H{9)927)&`H4@gf8XshBU`!np&c zeJAJH3Wb(@*$6S?6??V@vS_?A!S^5+hW$>92@-GL^7(@P4@L*1-sQ0! z)*zoJ;mf6md;b6pcGe_x-~0{aIo;vQJ0ha&+4|H9pK8gM)u5_zSj+R3r$qUVAF)#fxXJ@oTx3 z>$V(qmxD9;k^}9$+gyy3Es>uF&fE?CY-``M`9vqH+1-A^2gW%OjP*YP+x&w*(RFw8 zoD=YcB*pA`Eg)odlUW(hU`KqPY;L!|_(Wr1v5($+m#HHo>d=EMw`^CW&X4`H*NeB>cEY;D+O>p5Z}hsJhQQq>hl{lXe>Q#vEwkFk&UhuqZfrEU3O}gCK8|GZw5}VH~ElH5M+c4=i9bXJ?I6fuXMfuA=)*bxVBN?vU z)7C{J&2IUGWwbL44_o!^vQnz0OCjVu-D)5I37RX{{XXzveou(2#q&BFPY5WU`A2N z2QB*_Mm7iI*m~X>YCG`&e7?&>VZr?2yLIb5gRcj~Jj@AP9oNehyS8#;WiS%?vk0+G zn2(s;k2XdG*no0SlfM(W+c0I=aS3;2%v-;k?7MDFz_9ki=K0tMmN&^q#LrQO$M^Ik zS3ZdiJ?t5fA?)wVd-G;Kzvcn=vmzPn>99S^*Tw7O_WuB}x5d}uWJ}qb>~XwL_xwn^ zurH=vu*>3l;}Z?b?uGPZ?c(n8(i#kcU$6c6kiu`_@L7THsq+o^KM4W+MlF3A9AO^I z)?VGn^j@+T+BBQS@RGedIWWT;j1%Z)dm_iv=ELuI?{6~bM-JZnLMJVJ``Ej_Ko1y0 zSO>(Q*!T~&-f<$+Ss>=#z5VUXwq!s16T1d`aP+wPTXtwnX41Nbt#>0f12P_LsA(d? z0rYr|K8$g5K809Zk!4i512|De;mXkGF%iQGh=f%b&bOgP(sAZhs8^Y}+~1JH&emKik;&NGNQb}qB; z`PgV(+(>ypWC#e;iiHuVlN1BJ-J&xtv( zd#B!aIdf!XGW6g5v$k=y__WDu8o4gAedh+|%ey+3#+L%s zb(q)YN8t)TpFcZ&Pa9hD@!J}QU6tP~WAR#y$>Qqa$N|XVmZ6DH3l+$5(q3GRY&RoD z*Lfd_CuQZ2nbwmIPL1uQy~*?F3&9K*mhY*ppr0 zl98Zx*9#TfpECKq8wfhU_Fd~IZ8G((t@73K!`TYQak!mm8^yWUd2vQ(>-$=q{{R!!;gZ3wT3a(Wz~#YlDdOB4Y1uL!A@5kxq^H;D z4zhD^&kT8H7JN3_G0PcXYcL{lC{4dyGBKS-i}}7vYIjg2NwXa`SSqo zR{)WKhj@5+_;?TT@gEXC{IiG&9zG->eoRC}OhojU{PDkn{4psRIT`6=Dk^GfDk??> z1_s7w{}DKk9z7y?O!AbR{3$&R1r7aweE9#}-1h<~2youvynBE{0l=lec|d`4-v@XK z!1*Tt{~0&{+y?~r%K(CZdwaHG>tvNk0#Rf`z2zR?iB*pCf{AMRAQ`CVl%qtEV_aKK zQDtBaYNRpd9Zzu+sUFa(sxgVKU{;<|UC$NJYJk$*7!a@>FGCa2A*l zrcaMIX*@cI!swOePeOrJl8eH4b4E>Qyjc{tXcpLxqA8^UFKe{T0}D}bXhJ2DH&U@L z8rqW(%|7qj)+rdC%rYn>Uu4r~cjjhyDjm<+RMJ$oG0qthiHt3h!q^a%MrMHZjCr_b zNyB3^qe$57=Ja2(X(D4sR7C_N=L=Xh*>x}tZ4N3@#RZ=9HeN+i1+j>d5u-&y&3qQC zVoL-@URO#4qNxA8H#KWssX`P@BVRagh!WA$B-m_9HPFe5$*Pp%ZQ_W@;3Y_`5=I;2 zdR4qbu<5WDZ@^&jiWEZ50w~-PcGKlyj;cmyupZ?yZ{4(pqN2o1wt-HfVirO*rGikD zf?EJsRbj6x#f89RVvAuTnmTCWv?hYK;xUJ!mvcI_W!xffxkNO8K)q>FSUQD*6MMOF zMLrZx083N^K4w!yKPV=lglP~U^l1w*P1L9{B!iTd6}u)i3@C^x9m3>%6w}m)6d55H zq#zHi7;8lGQ`G3G%TeMIikdV!R0K1bbNZy-$^}Xk4zDKvE}ql<3n`E&vQ>_l+$vL` zUdm|{NfCQTKBv#2Yn8@M&mwOx%7u`eL@GDT`zc7eR0)Zu*@#M70Uugnpp-6WV})K- zqx3u~+$_};FQ;`19h&SMqDZ!18RTo)+F;o9fPYOR^+dVY6aLEPvh>nbMB`)VU3RdT z*b1ELtpd@vRl6R}QeX)$2X+1DX>DrsV^wwAdIehaYNn&k3`C;TTTWi=AN7ofg^;w) z20lQ^oQf)^C{YDLebKB$+gN&xXcjQe2?S;zcf}jGb0VR6Ii{=!Rhv>XaE6dP@6RtO zW)RZy6{SW%s<8Gtw&`A~Nb06Emz^eLL@7&gqQV?*UnJ^bd1l!kVo?7-r1%#-{zVF0 z^8cjD|L*>W2$20Zz&`k2VE>0`{+INBfMq7f0X+CuHUGay_fILOV4)P0XC)W<_kxat z4;WGIRAkVYBR@8iLoI+G)F^#;J(_udCRI8l-)PSJ=2G9(+-o&XCUTNbbu}^D@4sb` zkGG(KH;X@@r9{`NWC=4GDkUk(``T_p?J{0wk8s92uR%YsKPSuIo@-WAJkAVn54m*c z7neNeNM;#`E{eU?aH6Uc z^YHJ?o_YF)3^gJaaVUjNn2gqXzHt(GEB|^z$xDa;Lg&#Vpp29L_3u)xF)zBEbkTh+ zH|?6XECImcg>zEvqYo1)Sfg2L#@x!kN5z_(Egg)9GevBjH-mR<2vVmKn|*lwQe+i9 zo|#lQ*e!|A6MtHCm*znc(XxImrhHXce*ov?IM=8VXnqsNOIY-vrlAsm`c9>8X1ZJwPC4vH!+3VZ!FrWJ*K&QeJoKE9Q8#>@r1(Qw6D;!WN) zei_EF`-xhjzPb8f@lTee(uJP?rRecnAj`2$ATw3-K>%`|yoRN6D^Os=TWPrSvst*6 zbT{eORDZa}(}gJIrYEBT;wm>Pu3Hj+DK|R+#!n;)!j@n{*%LSWIPaBV zZo`+#gn3~@nFVj4(deTod*cf-U&ic-VN8*%ZR0DvUi|twrF3viikVEEgpA#ZAl-@d z6H?9Pauj(}WBlj>Nhbe~U#)PqayA*8z}>MCP{DMiBz5GzcTylY+HObkfjvR{O(tSB zhjUAS<1bbf)_iI4haDA#$yW)NN9geS@D;v?AAWjedIsznMV;`|u)N2DX&95-ctgmq z@hD|8<%gGqEhOy4_f8z7pIB^oWlM}=&FyD4oRYu&DwJW%zma|^ybpp&Km;~S>sM^; zBz}O7n+cl(@KCrW3UvZs))gOk4`T8%aUSsh_jTQr+CmhmkOn%PZ zbz9jdxEdpjsJ5QjIN~4d_~PpnM`Z1c7kvHtk;OZTmf@N!4$1jo{Fo}K^eO8qT@_1v zPnVl7cT!w}bW2G;zF_^>+9@bMjT@g6W_5Gc_1 zT-j3Rligg6Iic$*z-4Oj2hxGmJ*3&kk($00iPllkgXFD{ysVwkT%$p!=9(=&h0)%}wA0HyGR<4> znqH15M){fHG?ykB*Nc(u;Ve7I};x`XOTuEK*8 z%Sh@Q&%RFLdKE880{Lz#<@mDNjBJGW<9tfF z*icP)S0f`Bo0W9)<{8*@A)3G~z3M|)wW?^ZT}S*oY;^DVF6!FTt(bs8<}Aw1mqLor za?d5x`8kcZN9Kh1gi z?9aPLN*`gBi~OA;{+?p899cx`23`VFUWOIIER!{lmUg_CxtaT`D68ZBz~6RQVz}t&%X)Ltx(cGKB&OJ!Z#T4o;)a3& z#Ulb@o^F{`X3C_b(dof(^WGYo#%kPPqFG)%5hl(>8%X7mYxnbkS79K>MS;dIMoSsS z8-_OLc$^FNlh&jk2_m&@HkNK`RfT4o*#NnykTZ+PaP7Srft$Tw? z(^Key)VX&!1n&j3r>tiITU%pMsRl2Ce+ru8>_Pji=)FU;X$DDN6+wczzE>s+55tlaq~*-CkUjCyOSUnZWT#i>7y6tff_7Dp zBoh7K@X}?;`q{af(5->9`sboKDpt)N>jdvT08Cy|h>(H6IQoUN$X}B@CGka?2xh+lAn!_YbFK!e!$Y+y~TdG z8RN`r%%O-qP59>gt@^cf{nGhtUYVw& zxd<>2H8wM0B3z!bv7%uQV)%=4^{pY@9k;Z|=T{$h^mEwEEb>zYZ$U4y2G2&Dw|sVN zBt<7H7|C=eOTJpB?~ku|JvWiy6Ke8YH2U;9%DR56v*al;Q)WtoqH!xo|I**i_OEbk zAp0){n`>VaY+Y@|5soCWbU1Al-J##jB_pX^H`xM#Vb4?j)tc@>VH|3BxnPMaXmOby zv>Ax!L=lI)Z2dr9gRe8{+tZxGF?N69gXB}~4 zf&r_)1v!6yWMH9YJCmCGyH6mjoPdmk(?iWvOF48$5_=A&+b+8ek-1on8*S{%{vg+5 zy{Nf9A_ZT_cIl6oQ_Mt+vwzX6(p4_%Z2k(y23Fpg%-#b^8;XQp$BS3G+^LPVJm3HH zuxF{))(5|q?RcLc+k8)hNNP?|) zBWa2saeX3sLaJm?5A49UnTKVYF0Rn+0KIrquG}zNKbD#~1f`Oqc+InUF(r0_a%H7X zrSaMMw9#y-_1J2Cjbv+U&VpRz(};frTW*eqb!6aXC*xy0l+%NpsvXV~8y@u)miKK8 z)}>K>i}kLpU)xq)$oCJUv;r$r4cXyAGF9XGdSiM>+$J9!>^;DwKu@9Axw%Rv_0+aG z@M-Yjm?zjPkWkLcsxVVtn1F_3K==3T)KgL(<<&u(;q+4s56+#5-CoY73ftc)5`v!k zjZ1b@`;fSZaO`}Ku-O{__vI$KcanCKXCDaHh{y*`vKm$%7MZy0FMjwrb+u&`-fMX$ zn?2*ZBt+;q2s##WwS1P1Wn|{anexHrn%fr11Q9I+;#ywXq!fQYeP9DGnsxMi(Z1z;7Sj^Tt zQnw5w!hSq^Zk52JvQwg(-cBaPNz~^O{G0K{Ue}c5GygTePmmfp`#oTHvj40E{d~*JEPTJ-<$Ke;75~<7BBcRm$7hHFX9|sMg8A=s3WJ4z(6JB|j z#ILxOHIr(Kd7zU?csSq%iUkz>k7Mhl^$j`p3dFv?!}8X~nx}j~&l5=kg?G z%|?II=a>jKT``yoq`=VD6L=_#@96yc@O64)jD}5ltpx9;InZ;sBJn2trwL_fz;8}Z zG<2hHy@IQ{StVRDL>4OT;BkEnaejtK%dVH+@F{DfHOF@?%3}q0^Ea8kf_^5Na!qBD z09#Nx!v$1gP50M;@bDgLGr|tT2_^2+XXiC7I$v%FKFs-0KbkaQksLLNv#tt+E(es0_kDy5N&o)H$NHHAkn`EJU7Lw_ZIRarLXs= zu4w7FMTNi)Cwwvv+K0H#;q%s7mH5TjYEk5RmHso1;)!v6V2}F{Sm87RZWH&5KQ|(K z!&$xZgdw)?ZoHXm+g-!xD-kuAwv=?^OGAr%pYGtDzE?vo(22W}C+tUyQj!SY0RO_l*d|x6Ikd3%Z@OK4|`jyoFoTiqA^|~dY%yhkJg?1>`<$FCAWW6e~dtS2O2Tj zuKqYtOPK1R3hMdPe;#~b>E*zA6dLceQ_cEj_<~6;L!l9x_Z;f~h5gH0@!WoA2~ajt z=TN>F+ad1>>%*mpSrIcBhUJ!)@*X6PFr3RGDrO zS$*7$!R6Csz=BR#-{tuZr~^#1-w3Pl6D@h*F^g(wXITp_aB^yBPZ9Z)L;^nB^;BIj zq+T+;nBs;LZzh2)^ul-~7RHrlbzc;{)lX8*OXx&WRZx92jhE09lssW;Bb#6ydJ1eO zBk&?rlWKPHycxeM^^mQ!j@*y&+0{q^`(LMsoq|@8&i-+8J)a2PM>P4Vhhxedt4B!M z`5(l~4F2(Z!`=>9mO_rU zMXC#R%hF>Ttt*15J&#k_OFT3sq_Xo^jYD{cFIe3X$u~;-n1ikHhuZ<8O&5-#?q9kQ}TDpsyXKEl}r&bzX@r*L}lATW46Gw#Q;#bcfd>B^UD&*56Xr zaF1n4RZUzQe@At{*sf7M;h(8%=3O;hW-qRZR*^qd9yaT}VyoIk$&^LHY72~=-BZKZ zz0r)w^1L}Sht3)^@p6v2cr>pZQi#3khK24FjhLg!FGNPNXPg(a2Vl|fLozC${8XaN zsx6wNOm&k*-HQ5C$;O*g12$ zu)-ZPP$tG_U&o(n#{+LnlCp`3$N<7Gx@P6!$=&aM@g;ldBa614{;>rPr^Us$FYnS$ zUsxN3o(X)H$t|I7yBs}Vyk4k77#H;P0#ol8Mq>7jvvAmUO{V2W7 z(2sMdD&s7ht7L`v@{JNG96+bI8HXO1qED>eigx^B!4!5WI8HyW_fGfqhyaGhtq5t& zNpfw6;A?3Ko)0L#O!Vxr4SYusV-#S{%r+SU29+mAoEn^^QW~dGgXVAVXk?Ftn1nWrA}IPsnU^5gq* z6;k&kQk2r*S3E^wW?C|iWM$!!0=2$U4u2-c415DNKB+zsuKhryn#Vbw!p_;HI>}qd zoBF*C?7$YG%khlB^DA}b4Nb$+cZKG;%!Y%)hg>o5(X=&VkT@SM4E!d-tRtG1#hA1S zsVLf-QOe`prRLfq!m%nuGgXrEv83Y>O}1}7R_*IA&ZldZ<|vMF)!q^(u?{Hare1}f z0ka2VnoFonm$2he4oT*tFkF#KFX?&uh1d@WC%334h+eF5(QVE*^badAhhD{9*O&PW zO>a2uBv(o=Ps!_d9N&vRjP5wlN*u}UQK_ZU@1z> zkWYkuG6QA3@>^6$83azQU;s3{$%)g4-j;87r|`VLr7fQD;I#!ev{yYT90MjHTF4HJ zksgw=(Q|V2bRLad`cndu5DQN;_}`UxPt|&lsC=t-OfEPnegm6}RugOtXD5S(x`##| zUvPQ_(Z+>pZV7ncDbxuA$9=2GEHQsgCMtL)0Vuv(YrvM8pGym>ykS6 z3ail(+K#H0WAzQ=;%l_--sn{Iuk`>y+G)J-%viz2UYzU)O#7B6I;JN)k}b4Jz3>=slo%wM5RQV~M6I^3I>a{4g3fXn=jg$uV3db!k6Idk+5xb8L<@ zqdebm>G9*`2)jeb9ijLwmbo~D)k{eoj$s)MU^FNC1yVplxIap4V1pZ6E-7Q(28JoO z_>M$oJ^5Jz3Ku~TblPreHmm2+*tkJrD6gEnWNF{`&A9T#tvR<%YJ;2D_S2z~#OdOu zuQjSMs2?dd$q6g)@Y+okq2`w!vZ^DHZQk@l;}*zmgLVQ@F7UJP5ZVvu|4lo_aV?I# zN&(ZrVN2chs`9HtwEd9!S0fh~7S#6xR`Zh`WK$hL+r~z_>v($2(TsurHb|9UsOM>u zGUy<+0zW~xmwDW3Yh@c-xt^)yoKp_S)o|m zDbkbltF4>`lZa}*)oO6be(JcAtM)1QSw1h6ECM{|E)5y>Wm!>MYa>lGa%lJ3I#gOj zBi5{Lb&S0iTbQYPZnk~HB`dEG_D3(vG8g;B%Z7Mz9GtlGj!^7XQo4Z*PMNdM=Hs`B zt~|L1S9E9`vU-AOj(Ji9mk?LGI-zl&JtFk;ET(q)5S@d+=;7}Z{PTn~^f!`4fAe6j zbE^uAD%4%hl`!uE2|w5L8Z};1QCxE6j+`G38(k(fojoz6;o`g&^ZcxKfcHsA=Q43I zc6WYA9bw0mPN3&MM)88pyPB~=eOAV99p@uvW_jP3!80T&Fu1J=oI z1l{WcOi7+F)Q*zxIh*(K^Ih#M75)vwc=amdP8GK$)|9Lb&6m}~ubk|sBOE_Z(cF-T zMzBh+|Tcq`Vea4QW8KfZ&vwz6t z+jC<@soBDsp}|s`%bWtb?Qv>wLH}rQOXpi=S!PxB^$xd|L3S6EpVG#_8PvxAu=&Ck z4wDh$Rrb6_yC|0NeqStREx9(WZWCv?NufRx`=ZG<+S%vj$bGKGwi@;QLv5e8@Zwwg zT!OiFhVK^~r8M)ygTH*0@tKa)wTpm0DVmO<#U&P@{br{_7W=kw;ID(JJ{@75s%E5M zCW27rlJ7@~O2DAzUFQrq-YgBR^UCL zSZU!bL~BFpUD?2Xb}cu_Wg%rvZnW4R70cVm=F+Vorpxz4m&qL=h`0w7gwb;v4B0Jc z#xWhgiEvlU!A>P#PF0=p?8a$0+)xlfr~`Jw9L)SyGc;MBE@XXG;yCCYQkN;po_MT- zTcRSHLM763aOgzf>r0dbWI6D3}%Xxrt7a%?4QZGl@v8n%aI z2g*4+{!Cg;}ycTu;6!G$k1E|r zD`X{Nr9;QyXho4zhp*H585~#KEnOZ`Z&85(owJ;d4wZZzX z0qZ`+P?XmL7xb9Vg)_jJ&$th{$Dw<(v9VW(n%qU!E7b{keJ$2m%J&f*o+|Y>9Xd^- zOOK2*3j8^Mq-cBgm0u0|Y?Lzc=1bfNnhvOLB0{%^R_sa|&3}yey))vVYj2ig>1n-< zmm<4SltcYmiI1p^Aoi1-vTXdzVhx%Sje&4xyE66>_g?6NnUTqFM6K{!O^)+H5K9;Xn+NnL@J8jr?0He?!~tp{|Em%cUq;NP z5441Pl)+clq;>0^NX^0!mD%_3P0*{D8j1Ca=}60G2=~ck3ULHCuI8t#198f{n2wfX zs_%tN=I_u4T|=8vPGO?sy=%ipN!%5QU?|Gn-WkaiSX^EDJ|WPMO_ z;n;3(?O}!tTRGw{J+72Dd3S6*8#eUo(-Zc^ z{pBiJmeALis#bo^aCWECSFY->YN%eK0VnP^b$HxNJzs_KqyAwoqwMtBaPlv~8`G0> zk}uh7ZFjips~gn^>TXmF4S=T~fs%jJy2!!MZAH_T46hGAC}KQ^w)n^(+mCN~iFVhU z#_|&JGe&wb!xmv>paad*bVWI#(<-64W3~BmlK=s_5H529u42#$V&v-CYQ%%`1MNp zll%g`q@GBD?kD(2^bF~NS?Qk8b4+lRFJ(^v`q{4cY&Ix-4Ft+l;5N3&{Qd}8?Wc&t$Exb;hUH@U$KYaE!7 zEOfu`C%)^UM$Zq9+`GSu=tNsAuc(W_GE~f*qlD7gyX>c1gUwE0D%qh-6OKKeLkOGW znqtfBuld3_mJTz%k{{{)H}CtE=fMoWRs7i|TSlMM*dlh4RF=s$cjQ>xbV!@T#REC- z0aL-lisnKv3u9!338>ySAkGxh#Wr3htQSBP)<=B?tep{!Y^RW;%k#`I8~SujJ!V?Byb)8A1qY}?T*`q@KVKXrA%%2PFO_`N08 z0#lzrXgq?D-a#Km22i2}Va#p{Ke%cgS=#<7mkdK%ljdgtL2K<9Xzdj5Fp6zIv zCk-9Dh-NoAfi-krMNhfIp~;oOhmVpL!r^^+tc??Y#3^!!Vlew2Kys3(jCc4gatTeuPZ>En z^r!n7ev=_zTnC?Wy`!7ox&ty(Q;UbUE5e7C<{B&ERhV8 z=ySzQ!P$xFrS?e!heKZQi&EavkH8)BKh20RLVN6cbnbJK4Ra! z57zN|0utf&1*}k0<#{d~(e;^v2MZ4`o%V^8fOQPrGTJUwfV~J%3zG@t)eYJQE<$|j zJ>6`=PQ-&{;bIKumxCxH_dd-R@V+odNB3|p_2eG#g_2>JopbO_c(rt47k){DS+j1p z6CqI2qwv=Va3NxCAUj2C&1uHgRK3vJut$w?g&=?-=!DxYW*mO0+t1Cd)?u#tv1e}a z4jwd!&@+dKh~)GPShlEyT+wu^cgK8Pa&A*0?dP63(KZBg8!^lAnxr&=kBm*@z>z>I z4a3A=z`Nm@G-a1T*xnCC&C3*&X>A{5F%3GxZ`8&Iwd$53n%|I2{Q(Q35&p;~1`^te zR58t-VosMZq951V7%<>uDCtzYc7IQ%QF07a)!Q+)`-vZhCl!GDbGbl0FxSkZvv8uY zUJ)|}tz0h~0vUSmvwxpKRC-DtA41^rnj$)3@MNo+`OpB6PnzZ$W734nKf`_$|8=zZ bpMkZyf19Gj!I1+nYX3JGe>yt<%2@h8DTc8M literal 0 HcmV?d00001 diff --git a/public/assets/angular-material-assets/img/docArrow.png b/public/assets/angular-material-assets/img/docArrow.png new file mode 100644 index 0000000000000000000000000000000000000000..a3076539a2b31d1bd5b7cb51c4fb7e3700cbfcdf GIT binary patch literal 171 zcmeAS@N?(olHy`uVBq!ia0vp^ia@Ny!3HER7Hl{Oq>4OU978H@y}iCs(7{2%CGl^j z<1*%zOeac}r+OVZv+|$?=fV7#BF2R?WuNWk+E-w{&_CR?`KfDq(1EM3zB33iI!Sp% zeisvVytHO(W?TaUBNNMs?2e1J%74mOSb)YeoM2qEP@1vnq&~wPMZpF88^2ulyPYSz T&`zOw8A#gG)z4*}Q$iB}zvVnN literal 0 HcmV?d00001 diff --git a/public/assets/angular-material-assets/img/donut.jpg b/public/assets/angular-material-assets/img/donut.jpg new file mode 100644 index 0000000000000000000000000000000000000000..bd5b9c91d2ce1fc8cb00d71b69588a99a49f91ff GIT binary patch literal 44703 zcmdqIbx<8q&?kCHun;5=AUMID;4Z-}KyY_=F76Tngp0eobGZ=QA?U^3<>Kz{@c63U zZtbgATf5&^weOGDQ#CbbPEB`D)yz5FzhC$3{OcOvldPnSBmfQ$0DyZt0Iy2`F#sYw z0wMxDA|e7J5)vXZ$|n?*4=Y;Y48M_nZueftuF%{>$YN{^eFC1@b2;cO-J^KGN0{8CC z%-bXzCg9!wEpNBr-@p0sW&|D+iwuGFvk18owhmmU4{hLC} z_m}`-z(XF0HV1_^FMu`=1bDlC_y30>zlu#uNFYw^dpaR=3zRa!L$rOnz|OCzX+g6O zH6r6%tOv~yNNtWiYt8n*+eO}zlfs!zcAcIqvQUJSz)qoTSk1BdrKtC9wsKWnQwCa9 zZK@skxYoTwP+w~$>HIKiqhsS?!OU2T=Y-@HATgwPmz{gi+^X#t)8KC3vtxu8MfJXk zvBzJw`ru)Fs$70-VJjuVPW)@qh)<%YhXd!cSSQ(6c2*FTUJkQI*8|G55b3J*yQv%R zaB%P+j|mp_B|MshvBSGL$uHc@j2*EXW|6Y>%B#nO`k5s|Ks@hs6?^1Kvth(ra4h`tT2*d_zECJ8yaLLwAP?qQ zi+Y89-=i!u++|d(UIAX<@??RH6JO}^9N4*}lKgyZ4l$iYm$pp)R>_(RlOt-pA3U&} z@{*WvJ=Y7-eu44(Vu@H0+!rU2;JpG0xQoX~GZhsCj=jPnvn7=@lRFjvOc}^~7G4v- z+~~^KaUAd5m@*yiN&30giV0|ZX#RjDaVv>fS6`~e%*Mtbfvsxz|H1dl(Dfak2E?7? zE#VYV&h{Jgf^)`*<56r}NrL{ zJq((VTC!@58^f%9LS@YJ@0{=qN3X3JbR?vHvEI+GZ{InXEHS`{*K-A2D&~hc( zEBugfMNDzh)Q)EP;xn#$|NAoD77RwhqXr0ww54CjJ{QT7vu^M))4Rbiq zteEK)P^DkDFUSmAF9vnZP}$2d8W)*NZ2-{naSWy(e(k%`F_kBw{S9m#c(RoQUX-p7 zT%X!0TSVqFJyAn|6U0}#IptMXZ(9CUg_FAcv^BZHzH-)s6OARZb-M_~M&1@%<)o@_ zo);`Jj9}-x)tw7j&sZZa-c?paX(;518^#@s6G~taKUmaFi!A>u$Aa09Qa>?@W^vnr zI>|OlwYSA#L(IV1hXEq}Zx}OE)&_`zt`6icvYhZ6N5S_GP0sGs+<$B&_7*8~OY0 z=GcHMH|i`A$QV{b6gTD*BmXm=Kk#mre^&ccYk$t01MDhC`O|NL$iR|$mGdOWm+n;R zNsKQuC0f6M%l*&GXXyzxF}izpR0jW|{Dn%+HbPy34Cjcx=!Q9-;*!n;wv3>~nns6z zm!fyV6mq>_JxNyL?#j*QNDK4c7f{7ML0dPA&Ty;6UDsOWquTFJk|O=(L5u&V7JLqP z$(G9bJ)18}1$Gw}#^fg^QM>z#F&)!buK>}gTg4R)MzOVDzbib~UIC+YB!!b4LrdY_ zD>C&d*d?g*>hM8Jb9Ius)^W>{U#msXsv~n3e0qHOQF^cE%ydatYovWZb{z&18s*K| zrB~NAp#?`tGR&F3r#QUh5R^Ixcm~GC>d2;8uMOLSlxFGJL0(I*IAMfFy(?fb(J90hM>Pl%GwIP8t#HJZJV!Fm8HEs$N*`15J`T)BQ-@II%9)f6Tx0b#C!S>y=uozTMMAU>a@$8W`JT6cAvo^_%T(^B zlU@PhW4JRIvAufg=vZTIvt-h6M^AugHJynJxW5MzJglCbBqW+E80mAvgdU)Hju-tgbX(EThDY*rVZRj}12oKjRY`4Ea6+WwfW<{Kk_M%WOdvcahmwHp zP!VQ~c}TCO?KpOpw5lE9%tN;h1Mfj4LW(QGndBC+-qE8qHAM2nrjkDg4^$t5dfl~p zxPKJ0){wmdWJ*5@DxcsIQuDm1^p_v?g}32+u&a27v}m;SQou`^D6bAFjh6Tg&XEG}Qst3+a=XY;cEC#yhhzAWan;aIp)`p~WX)Kw1Nx>i%#J`LB!zB&-=zvO~ zD2q|AD`+5u+C!LL)DdMPG38j<2K%n={G<9LXTM7%iX~*l1WS>jkGL8-m@k0NE1(W3 zu-?gC`lg-z11fMKkNmt@zcU6A+RsF&y)S4?O&Hv?R6WWUhG?{7xvISaRQ9n(N+o}J zP@QQ@_u=#tA4)28m2|%gbr?-H+unj?8sI+gNmNvQue8Zgv0aBE=DNx=^G_%7HbwR! zm-ZdYRPWT_^R(q_AW#*V`YKJ8TDaOV=qhsX*Wac;-4}JF?nIZH{!ZDNBx~e}QR+IeB9GJSB^-QC;WUw8s5IN^96|6txG6lmHF)IG}Z zva51V%6{D2GqKQ#{(@Inc|)Kn-UZ`qGBiWs#ynLZ8001%F>Ny{cl8kzWrX}ls;}XL zBTxUpSWOc&R#PcM=6_X^>$&*?J}Np zpokVxXYo4?d3{Bb!sJeC)=%M zISBRL(eDyjJ%0so3KE5ppG3}tHu9F)Du{F|myO|>d|1@!sCt-n`{-YYgH&{&m1^X% zGz#o|EZ#r9BrbER{;$W141;CY7#h6=hnQA>7Y5v&28p}s$3fmiFNj$q`AFOa+WWl4 zQ8>dD*gntJn02Lv5&cyuxYI=XZj%WMy2&A zDIhFwy`{;rzG$B~G)o4N%`d29Q(>d#$B93Gw)##oW|=q2bblwKwiK2wtNZn#LMMCFALXpN*n4?hfE6gA>is++snc)oq$a1n7f!ej)7(m%?_bL9v_<5EUU;~{HtAO@^J_M0%Am@TnGQ!9 zxzuU$1XO8~F@2+;kyT4$0GPLkPbk%)U><5?T_71Wy*1V4S&@jDKp$atOpzMTvtV0@ zA)g`L5<;Ip-S$538f{VcK;2j_7*0G8RKL87Pm<`Qq=$e*nOAsJv5{R(Dm8Ijn{Ii0 zlq;R9Fjz;+Q^*D4npK{YrpoU<)t^{w8%5%bw}ZQ%buDJB%`bx13!Gf38Oq13Rc^lI_GN_+I(Ixexha&bm(9BXNP#G}&@RzAzUK1sw7| z#tDtbyO=e>3@wXcvTlgRo_=nkWQDB^H$h@|vTaF^ew-x0CJYG)r0P~Fy%B;k+Eli* zAHE^q(x_L)OP#(iDC6MkWN29sc#~xCDT0OgzVum6zM&&9uPIVbu}#D7LG+{dOvVVu zU_h`8GMWE~8HWq~3q$Es`u3FqD^`ieYHYt5CsUzK$s|+VV{l>=mZIc6smafH@rf=U z*8ZDv?=nt3zk?UkRjSP9st9q&-@ms^%d^#KVMERRa|>6km%R@7ah_N_P&YRKM$a5q z!%w{~s@gQ(JOQN0?r%E`8^rbpCvx>5&w`XbU}$Us;DvG~e|=2%kodDznZEY^Y z31tK>X4iIWH7j5{Zc& zAtuRLi$=5BG%W7E(<}_*_Up{gKDc}@dM?rj+Mx@235h?;@REoRGJX9T+$)_K3u>0bgcdEv4;Xmm+vNR8Qm{&*;(gg(W+I=+U|b0{M^dY5ReVzW>}rx}v)Z?AA5s z0`*06P@Drn|K@{M%2<{EePUO$hs@S}bU=|qz76K}iDEsLb1h(#BKqRkr1W`1!DG>X z^bvK~3-mUbb@Mwd5+kAtPT*b-Un}(sP^s&x z5{;sjJdiT|q=NVVFmFhudVyjGyQ(S z;U}1+SpzVBM23&X;GV*sssW#rKK#Ios7-UBP+0U)vj}}(iq2vNq`OC z!Ljz%p_LI)9&#(ocKrpEGpc=PUbB+g2h%Ug%*XItPG^|Rjj^!iSoI~UrL`sUW%388 zT(n;bxUaqyoiQFxogHG2dZErXXn=?G_XcO2NEwv4j+d*sc2gmwqQyROXzD`(N8{IQZx)5g(0cuU4aNL?w0{ZwP1B0mD_s4TyU=3OMI z_LMzS5!YPO9CC!(;s>>VLQpI4=V+eBJQ}>AQhdGWE*%}q{Ir)7xug>nLZp2SG=}1w z$)Ch*904X)YDys_xuGQw)2or#!D6zx_^qHLM1P^HYizgc5@1<%$$^8p?$_+KKl&;A zk8&g>dQv|U=09L2?Lf29e?*~pJC6$as0XXbtf}KeIcMstoGsnt9`9cKy)VIX(lF=5 zXeV#VI_CU|T5wO@w5uGyO(;I*5F6NJ6@N`NkFOuJ*O?e1g7g*Cb6pw2dbqg#Qi)he zv7*})-<>EzA>17l)%~E7^;@BQZ|TluDIO6#HAdYfcIUKUF=yl4@Ju1hs1~MSfYzs% z=EY@AhfH&>0}lW;8s1{JegEG^T`0k=oPL3j*(}pt!@>84PBaxG&t-DP{w}Q%PHx=dF!M1m_QYiV4kJH;_=h_zw z_Vx$)=Zb|7xS+2{)}GkLjggLYJz`&fmbrn&BkTNJrySAL9=c2YjYUwpQO4YXG@(Sc zj`=H5y#HRVL#c4;t$0hAs>9EPD=TYPMD?ggVF=NN=Tw>8C(Hb_M?!V@lh3lZN#F0h zsU<(sP=cdfZ>~4WYZl)AzsMf$oAb^Rk>GQ3UOWo2KTWVx6qh8|=JZ^VoTOAy%a7W9!ky@041pUkTI63{Q)m5!g#t~KMjYCblAIZy zVr+9eneh4FGDu-}D*L~15m?j8^{=TDja-;cDs!Uz9zQw>wXa4hXz5b*SfBrOF^Umc zn^j?_x4^rI+jBwiZs0taGgT^hFcmd4+1Dr7Fa5BsnNafy+nqWqf{P)Ie}+tm(q&Wk zj{17~WT~-h$*<`K99g?BAVaB85%r#duAc(-w;(%TbCB(-cMolZg5wtlqc=)Pht}7` zG=3|R6@`zx9b-7vI?O8JX0L#?di_rUbi4{yZIP2JI#3P^+5>#W!DQ#ac=^3wKdtr? zSKP(FTojZtNRU4SW<;AP)cW%ABM%nI2-J{lO$V{qOL!Gnfa3455 zBxu{G)0Fe;BxOCisjFWBkq;6l#RWP8CWM2t)prnrh$LNaug!n002NG* zFDn#AG;6pdsBKbrrB&G2%A&S1;AC`GBpd_RV=2WJVgpGFQA-3JM1N}}!bBeCNNQQA z==puPRw|&@XsGx!J>XK4xnqi%{HEu?bu4Z2PHo#)04~gfbdCd>V=<;)W*I+1 zoiZNJ*M3~~lF|?HI~Zh0kxs>#eqb3PGb&uvd{_Th0wq2U^ZQQT+HFl028Ay0MM&__ zVD2$G?=df_;>){c;xdz{c}(3{ShH}UBQyj3I>gYz-InT-Zn1kd?T+0+?l4OVDg|{E(k+2GrIi z#fR zDDy`!C8*plAT8+^OAuZs*nRtHrmwy(-%KZH2}b}FIdiPNu_zu1;-`ZPEtCzE8BSGE zt(QUSJ<45R8qM^82TlA!KkjgC`*Xwb;8$4F;QA$5riIkyKAkj_r_w8KPm3DZ6um-n zop}JmSkgAAp=2Uk9)=mNvIz>Kl*Vt-m&wT7xb1bnINwv(cI2B@&m_TSzS%D8iIAVA z2cQa1`r>(e%~a32G}VJVWLW!ilPQo8tI;D@aU&-5`bc=UJP8o5)CDkNug6Af^)ous zw9^DBTp_P`!eH!<)F=TYtS4g6Ag=WQ>Pt#BQ0tz9^?GSoKo+s$~C0+07T ze#E)DH4|?|=Emi$)oy_xP1|7Mb55R?s6CTTS87Tu9%19y3NM6T@|>y`B=a+$?U%-4 zE}xWmt&Vi@nMlmmiM_A)%qZ=z5`fy^vAT%NH?wGlSiQJ)oW(~0wPMU~?}8PqT=f~8 z!>hP{fC3NDQ>$lt{B0wW{=`KUBRIp#jcms2%JT|pN6c_IOL0ct7ctfvz5+}oVRy^J z3pB*R3Lf9lyE1}vAQU(_1!M>aC4U%A<~8CawUCMTZH)tL7~{SvnzNU?RQzp*jLSLH zQ4L4HxYk(XpR{%Zwf!ZnCatuY>Iz2nCOm}&c8YvBX?S*8etucZkMz+WP119DSFP1BXhFiuCqQ zWBT7xe$*b)WD^d+6FQLUEg z$Q@3bMR$&xHEV|3vLiCpT;_6YYt^tGxjpIb=5Rq?=+mRE*z3|X*iS5(!t7E~2 zlhGBgk?db%``d|_U(Ni}%9|oMd`l(U9K)t;3@aTvO%2+{_%l4LpuqM8sogi&w%KQd zs+>)>MQ@xP!&nmd^X!T_9__60Q_Hd4K4JTo$_37U#qjWdriuH)1_-_h^Cw2+TNGmwa#~%z-rcD$vk0AxAKVJc# z>En=g4VWcn2}VXH4;)IC>(g};Su|~JhRV&70jtu0!l~vM5u{5Tqvy@6yGo&v7XH-eP1WSovL+hhYQ@cgim~*qJF^+~ zL)?}VyZriJYjT?*mqcE{P$W{m4j=JZW)4Ae?jT@OY&`?|W|X{DG*T#?C>wu}T6^Pu z|8*PFItA9ZG_Du*iax2hEMIH!#!nULn->qO7Nd&=$%=MV!?Pzn7o#uEK?m~otB9_0 zEwI=SK~XWr7-^HpF=tW)??>}0lKctI$S*8%M1@o&glzTKM%jwz+H+kH;~9L8&=5`L zCw5wzL`S5rq@qZL&Xx{IsfVpAKWorr6VhNe?XeK4{)$v5hCiCdiT&W>5V~ir!rJOi zN9<}zm?3>Mcu~sR6ZjNuk#9Ut_t5K z<~*DDXl1XA4KM+q_&NZo%JZZ%@}rgHddj*{&>2#V-*>EB_quL0Px7)SQ!m&~a77UFO~>Yrn1) zG6^*R+#bWPRz~91otvn~OD45Gh9BC43r1m>8YuYym1+Hjil2^xbZVSS5z5+9=qg(` zqcc|D={|q`DZ_&uTwiGA)T6U3L8t#n$VndV8_Qd8HUEe+jx)$5g`IP(y|-ip+5)Le zHd({rZp5#ElrHa-F?tQ>bveatn){~=5q`E-xaE1L#Y*;(94&6EYQQ`U;rQRGBmPu+6g~MQZk! zpNm%!ai)uGfMQqMtKM7$enOJ2yC9)cR}a za1|oVYzlP075*T?6iYcunsuEd#A^76w?!(&0DdmL70FqS44tkdQUWTLV^}h#^DKz+ zt~XIHSDg+NfHe&n7fNAzTPWti4|nIqj1YL>4{mgZx_BE?brnuan;CU0|NP~pL3Zq_ zT8CGF4JzrO0ofj(YRBC%-}a5J*68d7s#ob!+9O@2X#_axI@)!$+fH;>H(O)BX(LJ} zMrZ;xDAefb`{O45gN6#!jNWvUreH)u2w}k4!31aq%wACt&35#DsjH#C_bh*0_m{Kg zV=AvcWGhH^7CNFaBU!kHz@<-?ZI80o4N_}nfT4rw$QpiHQqv%9$$^5Nfcg(RrK9wE z>v=$gFH4%s~tE&SP1$08LTZKZwxm((p@*Fvba zIzu%i6pV*d$6Q8-W2E;1kQ+%d+)H6BSVli-8b{@7*BNp&9T5)@?3HTPX(5u#ZIkAy zu!!kN`k@?trCDSxGzwc+wIEN`vo8I_ub?yUtmxPKLH8K#dZ)|)^Y|KmUr+9a$kxG! zV!d2!*gxIjp?()fG#ZI_fNnTR>jQJl0dwh;SqJW=HSVuH;&u!WH{Jcyi<+@s4GqpY z)=GF&wtP)(;x$dYtueJ}?5aSBO8+MBO=YDJv6MH4jFpmgoJVr0eK^Bz)4Mj`c4jiOsO%yo5>}xO)lJnKPL_4_Djr`<)zZHau8cKw9r z##%SI_^OY^9up~uX7?La5if3e?Zfv(bViEd-V$>?9|M+st;~G8LH}IRAD4(lJ2|{l z?JKAg)jcxXAF$YxL=7}g2c4pL1$-%@BtpNc zj_HbZ#py?FErkppxqz3Du&b-^R)Q{z#(xy7Lpi{5c*G;9=xQOJ6~h6lCM)DO)<(V+ z+Kbfo%oGv+rC<$?w$z^v6P%oVNM3<6*MDp`nHU2s{$6t$%c{vN&VX7Qq3Ca27@JNorB$LO(q<>#cC=Thox(BIIf*&5kQO$G zPV5)==i^a^@sC_DJ*3*D=4Mk*8DX0ez>6$ySPL!r=KY!VE&O@lJTj!N z38CTraTzTOy@1?SopoQy*0(uU9ZS4Zye0z0O!0+(^?=%D=ax;U1B1o{_nG=c-g2ZR zoK~OUs$>WAN`j_ul7$wfy#n<2LnIrSkR!t=HXFvM4UfU&#F)R!?+Dz{E1faB?_d1t zCMyRzuZ2L)UH)fjn(gXFQ0yC+nd1kFeJ%O=HmA7HylD6@O8uoD^-IrA21h!MdU?!< zc8Wt|3U90FjN}O%W2KB4X3TiE2y&^bs8;?x@{=OrFMAXmJw%S&OZR((qV8t-sipe_ ztL~3Y;TUS!-}!#oF>}Yv8Q_Rq7M&5Vb`n&q6Ni>VZO4addYHD!_@5>Cr@wVrF2T=F zB(&j8BoI6b zi1AqS-YK~9?rE=&vg|~GU8hJ?+NLe8A_!Wh{F!`)j;2dmxd@gir#uYjgrcp^rYye? zVaPB~TxJMKbQBSoc9&ERQt~o4nfjb<-GgkKefX0*UdDmeth@MtUUn})j!F5U=~TWT z+`3b%@fDCYqX;!wQnuc=tCyip-^+?FffyUs{0O^XWLKC{|Jxy2^&|Q;C_`hgA$g9j zgsFT|8gyaj`6;r;YkDRke*ZF3N4_Z*Uf7S#?}7ACKS_w+ajQV=o0rh9#z|Z4Exo$7 zmQkUjB2Jj_r3ptLda(}SBUoqNBk~pCGJe^Fd<}nFz`=j-z%gHxNGW=NP5{;zswuJ8 zHe515Z+g&3zqn7@m@Pzq0u3MsmtCHg&O6{-9n(RX7qn0$Cm%A@|6s-;Txbif6#m9E*sG20`^@!}N4! z+ap0ny8af6Xd# zwgN+Gy|H9Q{pmvv6Ir*0G?mw#lK1W}Zp$mnErJwk1E~#jm%K3pkD-GV+G@*sc7?iH zdR@k}LrvHhzn#kIOQ+M&t|8FlOs-{tFRUpyeQXmrPKaObrQegvjtk*fYVf%w+!Dl) zpKLe$b!ZJ5Cw(hsSGNu`p{uk_r(hdc7@Ex%ea3%FBdbrIyutB2qS`z+>#ErX5dClE zWUckyo)h{sdgbLj6pIPJ%_hD?WcelX*cpGi2;w=_w?8G*r^+VM@Eg`KYl_Z{Hj@+C z_x&cpc*7Ojv-|6LiDW@wBh{5uRd?PXU5-)$Ij1&?+cf(-{c=@22^V#<(mT=8 z5LWRig6|v5AcZ8a{(=|(%jRY7?JL0d^Ifym{oTr*Z{hXtN#f+6A%a6v&^>GQN2T+t zCW~61q{uWbk~^Mf8=fgnyiKjx1*2uIc$S6eBt?k+gZ9)bU^BouF@6-Oe})tzHPQ4= zzxg>6gCS5}>7HTlevyrY$H$5?#GL9!wNu4v@7=>pW#L=V??jcY_zIar>XAUgqI(@04}p;kx#w+7&h zp>0`Ns_pyr@$*gcBthm*QsF6CaGD^vF*=2$5nni1UeDVabC3IG2!AU!ONvq>yj!DB zT~Exncoj`~_V%Z|W`7j5JC&L3{aa@Nix6}b=Z5O{7|_gtqbem!9DO2tozeowoBy3= zhK~0_p&MNkHK4*;=;Tb|V{PQNQk-w;)@ePLFnyVI^KZIu3p+wulCafar=vK~5~Bu* zq8O8`bRECbYy)$(JE3O8G`8D@a?9wEJJSy5@(U_kp2*I}TLtIrM6XMF=D8jHBSCPm z>+-60@le>LU10YYQ2r_rqj!!cqalFLHzFjg(G)$seZgxZQex zHlN6Un=o&*vei~DTdL|4y0Jn?bsP#l-J_EW!!dzNpL>t4`K^9&L8 zm-Rw*(}8)(SmrGLpXlqXXk!ciWZ>exDBVS$>!r7Vc@j~J|D71GZVPV|CTibAa%r}T zen-nDUvt0MeIdB&Kl(7n^OA^mv$7ET5pUj}sPsUSe+h(Ur$uEy8SMX$~~ zT#u{Mw)Uy^2n8G;2lqIV?e!vpkF}^ zrgYifxZw|MG|L&4`QW&VucLO(lx z+?EzQ!qm!_^g`8}XDOSd5#F}g*zC`UjOfb`UNgJmQI_Rj<%-bQ@q;s~nLv4z?kj-kp6_7yQpbpXnd z<@Vr$4A3F>J5ZKk%J=?QR_auX9EVYwW43F#yR7zdjG^b>I^MSGR8@oq^P0L+5X4JU z?~}NQ&n?M}=t20Q9rZNKKz2uPNu6`6hIIX1#b>TFkDVHfaCP>P{DP$Zle2-=vMlL7 zbR>us(=!q`UK@^FdQ`@oeP?T(4ULoVImuW2-N>G|636FU7xQ%UAx5|UBcG+^)3MRN zf3h}+CBzLw%$LRxY!vQ*)3*eE`40K|r#Ac%;Z9I$?$d^ssj=}{qj-)+TQDkQC?Yak za*84~g*$-;52vp(Z>jc?N<0?W$so5d7DK|d=%+?zV?S3|kT~j%s{B($JJtzz5Cdqc zpKXLt`Aj?U;YA6QEXt~CUoq`a?m%AKknp|w^W$lXu={qe;C`gCl>>EMoLl1`$!f~T z+YlK9=R@`U-|HF!q!M)Zc%!d?5S<14$lp#UMzKGA^@Pv6p}{*lh*B~$BFeI zFUX&(;MnVyR+6432SNIhF=Xf)AkuDuI z2nlIC`hH&ZQSYo{#H2%GDA5 z16OvVTd44hygGyFHbn#)G*C?_su3;kdwUgopU3?PX}(&R84qePD!=NrU>Bv^w~_co zv!CBOSJ5^cXMBMVAT-=M?CO*$rEYyX3l^#a`Xo%s1^B{V*b(vcp6Hij42F4RTTsIXx zD86uo*oGRu!TM5Zw(CE?4Yu|7N*<=QRm2vLdHd3YI-s1G_bHBJsAREfNfw?*DN#T| zLdlfom*vJ@5=mjjV3MJZ&i59KbliqEi2mVemps7Yg+Cp!Cb7{Q?m)f$SHPz_K^tZE zrRBP6i3pw2i)^2_K7l&(CpkLZqVSXy=fU_0(!v@?;E2ahJ{(|-HUIZ%Qk6+N=I!XN zIMwBWBmZ8R(j73hMg|e(#>*cDp~!JNPK{~ERSQSEb5ju}^U>q()LCg?SAZa${Ibt} z>oRE3P1b+w#yU!@A0+PuH{*%}oxm8Ri#hfd3LHfYQKsUX6Sn82f^lOQKBYp_$IW9BmyXir-C#O=s>V%NM%icVB40>`JxSZ4_A7wP ze)hMi);IUo=&E<;m&IIvmvnwLVlcB&B2A{jihnufz;^@q-AWF%BKV(PoVORH5d0S# zRzJA?($T}ZRd$rk*2T>7RuOkkIAL1$x+$QHv6o7D+h$tYZ=ZHrnws2}DCr2w;4(X= zu#;`f1<{pxow(Ud@JX(hLDFAKB;KDxHdNl=xGYs7xF6HH?BU4dRDIff07dYDR zrbzjN(+B0)+<8W%cnt7=SKx~xT8-;R)DL+Ju;HRw97kK*zl_4Fq z8R|ORig=IZ2H;=x>zST2Pxgq)O{hV8I-QwkT1f(MXKD4Y)=H3T#MO%Eg1Yzm zRbQc`RH4*93$op`H%g4>JJ3NNpI*ix)xecJv65*W>z_ZyEeb$T1Pu|&0N-u}r;UAH z;}nM#3G%TUxS4kOL{gDvGJpLpcsl7({1_GXxUTGv#j4c$r#K(15jap?XX?7@_sBiM zHNs53O8ML3FZQC_C*1*(<4#4d(kpH)zm?iX34@vyokXlYq5YMGw|?3pX5AxxCz;ZG zDymwi6KLjV*ZJDO>%PIT?Zr8p>N?{t}9dGH(!);c+e@;pP)GS z=O%J3GaNQa+l(eEztlqgyK$ns?GHV|SsVj@jjU(0KG8#FN7)W1_64Ls-Gk6= zlc$a|UztLi2m3Ohi92?5M9M&S^ixsMXL;`PE&8Kd>-$%L;@Mj3)b3Q4Vjb0>Ez$F; z!Rh_ngtmN7)e`*DzvHV?tA?!`CxPPVD5ALVMRU8ERn+6kZ!Z(JJBx>8srmTo_nWZv zeNqep*ukuKc&Q$w;QTX@vvzBETk290CuylJQ)X+tFfvH1J-~E2iuDyh?YRAedmwiv zB>Btyr&EW|4sT19wE|&C*mtp@i*h4Bc7?Q%oti+GyiLb*G|3f8;XYU(KI z#5}`(N!6$xbra5S;)x@Q1+JbJ2nW@p;a^SS8!gi8-s+;P%1`YG@`Yix7uKP(f_AR? zFu}s4&Tpixt}6{^1$>Gdlyl9!71`w}c)CQCf6|eK0^5)U!NNmx>oo zCRt?O`@Z3HcjK~Wjl4|#0UvTKk0l>lZ;Ravuv9c=HI0DD_@vUe1QgJNi4PIYe&SxU z=A}w>t$D1~;&k%gr2+HUf7KMNg`Gs7Jl^kX6F0AWp2`WB#0Sc6Wm1f807#0!8yjMn z99g%IjN3bkOpr>0+)nHQ)AOi}^j^09+}Ol~hoD%w+?|S|w7tA3Jys8lm#?*u#@GPG ztFm2f_%lk@%>ZuW;zHxnf0U1KKlM~l|L(`hksFiq3J+aFVwnZ%jLp8NkWgo=lIia` zr81GDdR|xRVC|o%p6Elj@?R9p_C(9h63Hd{G`m`pAs#&kgvVMB2k} zc`HtEz6Se94cwnYD;;JVMqARm_Q8qQF=ZIfVow?a*OQ zbDrfTP+b#a${Z@Iy$U;R>#wLw?3ql{nAWL%kBLGPp@8W9JGBS=Zk_TVetDnEaj)Jk zo34X(kj`J0vdWeB&yfbMHpH@ixoQrzZLO1eO9P+pjRC&~hxX?)9;Q=KnhVx73bgXS z>}Ky$HZRod@%?tBV2B~?m{u*X3C}^f$ge8V!Ber$c&}`|uJJfWF2w3HI`VuJxYBEd zLsB(q-kipJoKRcF#d7bZ%9WXOpif?Pwhk@beNYF(!euNu>Mi2V{lR;Bg(%APH>6Yy%BO$0RvTgZ{#UohXHxdG z%MIkra&-PoZc7kjQqG{$_if;4@snI@v;1bt8^j1({a)Az73?I{$1@nNQpG1|{i;{Q1b5aPfXBdqU&~ z#+~Xu`ohJNootI;TlA`9#-AT9X7ERYd-Ec`dWxIrp~v!4E!HMo)}<8*%Nw0q&git1 zf|3_+6WP17_}f}~`tHl?N%bed`o0ObJ_RYg4jwey?0F$l z9X~9q@U9EE=m~1DMt^1c{DK~u_~f<98h6vhz>u0CFUoqw&ts{Qm(ZNf$pDDRTzDDS zkekKUp%myX&Wa)auJ0F_56$=GXWv=ncOAcR3%fHAMJWVtlodZd?ZmX(2%^Ebc+ac= z=yWbMgHFtheZCA;RcNZCUX)Ph7t6>QosL(8=4vN!x?PnbI{?Ge<(<(tktnx!95f~? zs0ocr^>lL*N2kMLHTU@3LNa~0;Lx>2%ISc%HXA#`Eu+T9#w}?TJs|wQ#mIQ>7^Nf9 zMG`alm9^jU=>Eq4EW&0s+R0Fx0?5YfCEI$fX6 zHaq~YOVa;-q@N&c5qB}UW-aun8s4?;jQWhAwTwD1l5U_*xve(YMe|s1k`2H@-QZ{D zi!0sbZSw;g^Wh@WFZR^Lc%KgY+@J3m-tb7e=7Vgaf}g)VCs~;f1+j z#hLDNo?3oELh~Z?DG)2+?n2WRI@ttpJhAz;?MBdQE~zC5&qf@>V^NZVMuoq{+hNdQ zC{Z&L)B~G>$O4tG5-P!u%ojdDolywmk{>H*L+lAyLn)K zPaJYCjY%2Py{&KrGt4lMcB253^Ep*$Gf2nm-E24ion4)X@gSV)i{tyl(=@BIarzIf zc^y$HuxZ-glA;yYII;p|XRoxuh8qxGG|5r>1^ESda7CLAb*dZjLQ4A=GYpoXNNAe`P8w2+>hx?#8LBykS?V{rb*(&yA0jXGho2pQ z`E2mUP(kE;h7)B-cF5bLy4>%yuj8s`kT8>#u_`A;gmfAO)jM~N;fN{659jayXfbTl zfK?gF^)u{l=rjIS`JA#079<$TjZvfmb}o*A*kdBf4xRG4X#i}GwKFCW6k{P%7aX(< zA%y`E*}r{C#~?+b_LjY7nUo7d4t(=ga}8{F@M_LH0);N7P|*d-*2{}@j7r_9d6F>FXkP2;g0kK0cbR979XXB~duu{O0{2ZmzzVQpa1{-0hK0vB z_OVbHXD)-O$6V^&zjN@B>+d!vv%m|1BlM1B#(qI8S+{bB>EFl4E42JaukLTl@OmRS zY^3VWuMI4shu|Fw9FpvR6;}JH`spa}43{eJHMJLY@@J=r`CYh1ONrog1qitCmz(+8 ze6IEIlIp$~XE?eW|ZkK{`^=@$Br4mO=cuzpU;xS|;<8gULas z)|k#z61%7jar;;A0-6R(TpoGM?`ySe->qzS4NT;(SHuF6O8qInXDD^Hoy#bG?$xkp z(R?<{Eq@G8y%7+j*A-{CE21Q?!Ht(*$ zrwj<)JIV0~95nzCraI9kWJ*G5gefxn_Fy%nY*3giRX zxDc=>pcP3k%0VPpn~GU4hPtob2X8yJgP1(8*1mTB32d{f@aeUyzh^-iJ+9XE z4400O&|n|OdfAtkJi0$0pcsIw3&ry`Ff!o$9~yF=qoVp*+z!XajQhk-BZL zKi_<(c@K!aQcB%KjS5V!-{H;r!n|+~be7Wv$?k4U3Cn$FC~Va~xA|)sz8dmt}J&~t>UIT>!*IAxgX}cO&WEH>vRNuW2*ssUpFH3 za~welyT}|jU5Ncd4?Etv3EHd1(1Wp5QmBidK3GlZ%6Q40sItpC%ik|%4}=PgBhDxC zQUjQ>Q=*55bC8j!8N5e@vqC$gfGb%ZwVw^B{kV4!$j-Oya85O^WuO3GRn=;nRGvlP z=FW1Z&!*aBfQPlg+tK2{(q`FZ`wt@@)HJWFpJ}x5B{kHKI8oM*F^uu#eb4FN)r+7M zJ8|46{ZU084f3CzaE@wKbvZ*-mWK=Bj37@bs4u2tfnuLJOU?t<-wvl!%g|_O z3qPHHZ=Hl;L@)WcI{cL_Y=_xvUF0_ACG8txD2AGqMq0 zY3!tX-FR3_AH{1FYToXN*G3wXx-(nZ{l#>=SnY;i8<#ij)dZ$LwP|1T0~@vx^AFEh z`S-ZJvcl!m(1;s=4-ZmY31(E}zMyM8_jxX9Vpp2+W_#TlWlT%??a$QaU3t20@RPla z`W+k5(#pM08EoJm+BX=oTG!Yf!p%_9)R{04PiABjw0}m0$##5pXtb|p*Inxys4{~y zWMSh()csRV;6$VKiA`d#(k?YwCjFqG_k3QRGGwTdPD&6M5fqT$sAS|XQ41Jf9Z0yP zpiyd@(sB)_^;ArXFbVz3u}bE6n{+}sLY-ottV*CP%li3P>&?g?B{wy$YNrz#Uf?L5 z14-1_vEl$_@J7{0Zgu|OT-==&3 z4y-GzaKEmss(9P>dW)^y{sEae8)@jbELHz{@g10aRy)hgBKdeQGK3WUk3DZCa(=S7 z4Qw>F%UPIA7)@m;@7!b4Z0=@w%FXZQdlGUNJa=`_lzd@`&u~5DdbW2S8%QiRSt#c( zc+@@)*t(g~YC$=X4d_;>6xw6=578{i;Ho!`bCbuFiOyu`E6E@`wcX zDWPde;QUcS4q8gpX!f^}4dS92dbHq#D>|&syjEtHG^sWLyHv$#F~C)dJJusmuXmwz z(o8`edc&ngh}FndNi#OtV2r|fzK8bOev4hhq^uSNSh!80?TGy|-M8s&@-jOe=8A%p zd~sL<=FX5xUenQ|Fh9Ye^61+nzNk6v9 zf=T1nTkek!*I=s+hb!FcG_A4)4p+;uGq0onW$Ob0u{INDWn&#bm<3v=U`cbp+KilP{-b~=Sq&pccK>gqz7LrO)C`Fui>`NJ0$yH<{+_YiwGqZtydrv8s2 z{-X;g_7buOtG{kCA}a4k^heLd8xFJVt$vmSGTjsJ+y2qN+$JIxoeC21)nXh^!%$`a zjtOe-9?fBO(!jJIiguhfNgc7cpy2cj^5NrfV_lLW%o>%OI@6p+CmW{F2E?uQcrwVF z!KKwLe_#%av9g(H{H6Adi>hgSJA1`*c$J#s{{4+<6{|g6E4}XMQPYr08Haw!UKz+l zbZKzbOjm}Ubtj-9I;mo{-vh&ok;ObG^!%aRJQFBWbF3vC=^b7Xh!yci+X`RZqTjL) zG0l+hjXd*aueuYP|5)UwLC5gsw;K%I_Y5mxV$ZtH2X6$7*Gn-r7|E~7NMq{r!JLVj zvhGT~|1%kTk@l#S6k2W{cFIm3_MD${z(#YVXgZ+d|9Z*z@s9cd0KL$NXxNAtUV*|P$^ys)sd2R&$W}~z(P-w<&6=Z}yAhjHddP@F7Lzk1o5+YKb zIZY#*fASg*+>3EGiTU@|Cv@cZ^V6$Pxs3Gxqw7NveGTsz-bcIAw;5Ok9|hFK zj~<=+If)OErVHi^@{*9*+&|zwsOpOo91*Op1EEBJW)@}2W60`yJB5(e)nizi#e0su zwQ}FWjryr(GciY5{9$7Gv|#xp8dryf{L9?0FHG%EvRQ;|6m&YG@pUZYy;o85e+!C%BfnBfU=l0-l>vUU;dzk9k4mJ0*B=*t&sy-^3}$QKIGoE`8|S{H+z-l z)0N3-&Cf@Vj=;F+8|fp%3KIT4(wbIs&zugo!*CnI@tNdNKJu2ABYiHL83&z5fQi8> zzSjNF#d#v#m+~g?6R~63yBcI9rRAhY!fToH?uS6G$Hht>QA)DrvXg*rlZh&>oTS-O zDdF`Q{;hV(Z@(Q?*UJ{l`uqqiG9E~o*sVd^HvOI3XtULm4)K3Dh#)KbS# zg?SFnTkAIxZo&`wzW>m61tmh9ZsQ97FbeUDVkWQn7x?b)YFKRsVo?+M*>V0fOBlWO zpMO~VHAyiwh+Ak0$C;`d$4+8}X6}4gvzptTM;Zir6Tok>T{x9o)B@Sl?JC!7JGEzE z0F5!C1r5SfOTF_{Pa(=ufwo}jDVY>0&!E?Hx)wi2=w5t7)7?qm^}aU}U|yu;IZL=C z+SMboW9&Y+X*2D2Qn4>5>YVEw^)aAafY#e?yY_C2uP4d-%O0*;6bf4aJKG;#S?H_a zFpOsXAzCEdNZ!x9IWvSRU;md}PTDn1mio$eCmNML|1_i<_8YE6DP=zQRiSz-atPP8 z-|M<6gs-oasj^_gVmR@6GDrMFKTf*g`Y=FoH?}13Io4G2`s^;eb5*E`h5bEPXr<#_ zKtr+i((>WVA|zCB`#mqq$1m9TIv(SlXF=1s?^!Q(DAM=$yjgaa*;Bwl36FZ!_mr%n z*X|55c8Pb@BZc)&hh3$0Eq~cFeE<#;yeJaE@_6Le&C3}@FqYv9{Y8r><3kVj>0qa15X;GMFX0`WpGe{^jbdR zEXvb!OyD0Hk)58f*>V>;$ytZ9s~&ni-Wb&!`J5pnEaY`t`fE;f=Vo3(?ee>YyS*^(~7m=rN$iuI@2)j^XaukIyyI&JBEqfT-p@emQ({P$p^fm>Cu! z{iE7yM79KxFf{4cGFn)ZPh>-(Uu(ER-->7CW6uz9bUsG0+xFb|z0dTc!aFo#zhLL6 zA+n<3$%%WuZqlIxR=tDJdBq6okuw}gi78)T-#E?k0|28liEER*W?v?FxYrVA+K!Hp zLgvI8P!RD-yAZ3}5lMG(r%vNYLnpau-5eK3VMjaa?%-h-gn2=6@lAan0Ff}6YHL>_ zNv?&rg>(o+YX}Uf3(|YNYK_zLuWa=|3InvY1|##e|Dj!?0$>Z9MXfDe(OY}FMIybd ztyrYg1~WdlDal?{sHKZa^l$^@8~kxBC8O zaV_r5TIyoHqJ@*1XsIvDw`noARrIb*Y+b5HA_(b}*P2X9{*5>0!s>k@>rDl1Ri&J} zY1S{%G=gybvimA(9|gS6oaviqodGQCekjk>$iss33{ljvL23on^m@3eVs8gz*ym%yNN;l?ywr8S{;IWE1XuZ4#O5 zkk^yAh7i_^yTm+ETLP<1MR_o0D^|s_#n%%8(;ID9m5Yul-Y*@BqCVEKK(+w+2Y?yG zs>1I66=BB@T<_KAYl<1eVca|Vrc|7$4zuz~b}UY#%718oclw9dmFFU!i~ruFyr%6( zVyRlYM$(B=t+Q4lfV+cx->n!nIkrXn;gWz4UG87>J2@Iot&#f5yiKIk@D-L-r*WtPcl@*lt? z%15~459UGXEylUo_h;jvHx=D#R_{uF=HO8oXQt1zF*Mscj0naJ;T3=Gg&dT|HmFa@ z63w#yuO!ebs|zLlf?0STj=#e8lAK7g5cSM;MNBDSQEvQu&(|HT@XYz(3tSs9QxL%r z7_05U;w##7qw29RMsq!*u!|k$B5edagfQvk1xC2 z1zg9i(=Ujixg{(m*D+fkU_!H?&|@RzmcuuwjYLJ#M%1Iil^3Vhw)24vsh{n8cBl>G zqt!VyklOkN@cC)LGU(ls>!4kXvE$~x$;E({mguR(eNT=k)0z2L(%sbBK>2+Y@h`3!SsKwea<7*sfi|UA#I?v0jwPM?SHgS*)4k0Uw zrLCrMQ-61%*rjfj*-^ctD2Bzol{VrjQ%I+=?3bYadYspffrpocReuMDH!iv!rf<62 z{qSz4KQOfc{3d}mE`45zw^I@s4kC1TzbQY_X%T*~sdTLp;BpD`eTp+8ZHE00Hl*bw z`ciSVWhzswX;2#VLr-G|`jL#{R{OG&Z^0(vewyS{MO>VoSsb%+JR~(zOWbVQz2T|x zf%e(RLGvX%c{2rkb{2D6?EBnWW^5~vE|c=Qy1!3~7dsb~c2K}y_ws*iEvE(|#;=nr zy&yJklCgu^WF<$_^D>U3q%J=v`3vS+T{&G5BhVjbJxt>~wH#Vq91y-^AHUU5CV$RL zae?#7+)+;<3O4|Zd04%{#@9{WZ<5xx04&_9E->K|J29O~78`8{PY zLpub_){Tb{vCh!xPJb2dP}<9^AJwaeHQ={Zc#Zvfd@OG_)^o2QFGK?^|79C@@S$-# zeK8Zm4KRiyZ3rmX30Md&+FrR49OI^!O0y zn(Epr6b*NWJ#5E*p6_p$=*BoNuu;}xa{8?LB(Af*vAr%CdzdzRO?K&G^mkI&MBTDU zx;{fTBQ`^(wy+_lah-GyYrNl6KvK~s8`UaBYGD_Swj??tUw$Pvg}s8VKVw?)8;qiFXG%D$FGHFsdu=)5Y#wHl(Bk0Tr^) z-f80Jmtj1E>I1mQ|DX2+^Rx+VbxI zh+#>!m8szRRtGs#UfE_ppv>wGdrw)@gDa3@{P4nHDaJ7xtmqUcVE8K6&4$9Yvz_A2 zn}>W+3cv8KrQ)#njBbrm`v+{F4+DSAw}l)u&E-aqWNr!R{!)414>*H0Pd7-q|8`5S zb=L0Wcp~EFKCWywU8E^~+4-uJ%Z{$kqHByt2k6HU1C z0?S^=|LnRx`09{rSK~aVd~B3)K!x{8(V|fTk|!wAS+A$9Fha`s>!3CYTx73h^ikOG z=m^5d-@!TA8dU$ZXMC@CKWDndq_=he^PD$ywA=nuBu#gti%H>sEQ!CND^e}&Nol`E z6vuc$EZL;b-TLBeeL6pT>7X^j1CK7N{DKsLX|Q4so3=y7gu<}erxk{H#B{@2Dv5Bv zl;tS;i_aiE0_M<_IgUB!lL0Fr_=yB-qwKB2r?vKvmjrp9%`pduC?ikHrDdclO|2=hnYJ5bM;OnH=1ey;ouNK+b1QNEJL=i7V-~rT zr(le$%1($C)+fzHLaCP^HsW%2FeJlPXQbd@Vc#^xO4Apkx{ZY5|0GU9i>|otjd=!MO2F07Jsh~1Dwhi3Tt^$gY?w{e&lW4 z)mkjYr!;yUDoiYB8}PAF+LgZLm2Gdmj(S6Fs&rp%nzg_5XSXQ!{+H0s{QU((z*Js` zP>+`;a-Oo(K$79 zjjV$0ozuXCtS52^%-MiO{Jbw5@UMC%QqlVely2_;$##uu>}4fsa(fopfC+2wO{l4? zoVW$rq|>LarIA*u=UOtUwZ`0OIZF@`B`jF;yrKH4;4<0im_=#nh!E_M{ADA~i+-5h ze>rH}qi(~+oS7HSR-yI3FiSWyTh|q!@uGE%fta>e{z%B?KeU?)-IE-dUyeR)v&w39dM#cX)!rm@RQ0#YSy&~Z;L)-Hmza-_w zM?f7Qq2TI40+htTFK3idfIvntseAf%+@EC(+rOr^1V2Na8BLH;q=;&nO;{9Z*AY*ukH|5B$X9JjWrYoMv=D0nG;?n5O z3+0yS{puT@Am#gqTvkRJH_(6aEDhq9tnjdPHPBWehf;kkZua@$o6AYZ5abc2`Op)>S8krdlxkF2xlCX*t% z#K7(-7ulW`L>0Ybu$q=F)reAEY=5*}1{L#@yKJS{r-m0h&QOU$*7@QDf=Is)Qmg5- zgK}=;A_TeO-70#t&cow%9+mhc^X0;Iby5A{BW+NHbjkph!Tgc;RU}`x`5HrT^pUmW zbPSnb$S&qF@DI)}joF=c<~{AJ`Qvptu3}plvZ5)!_sx3+BOhwdCLsSx|4SAGY2nxU zXx`l~EL$a{X^Le*vzQn%0vlzqa(aBSv?|!ss49W8`^tou@CdEcjU4PXXWGC*#R`K_} z*dA&uFn|2D&Z%wl!eK~_FtqtIY|5q6Pn@&~izmgJ;I z`2E~Jw7U%T2O-GRCw55$G~*ogWBE77F!KnNR1y@kyXOXsyp(TmrQVr+boj>h8Fo?0 zBf*qj2j73!I3OF0i^wd-8St!H57&fyNvWOjDY@}mK_C}Yx5d}NW`%Y&Yoe|d^`enI zoNiF+kI0$z(C&9TM~i1DM}tLuTcb|Gtrm$qU>g=IS66^`Xb%g12) z;`Rg|i5ZGG5~7|*HV*IB1q zMas6=1BoAAtBBjms{6M+n$9oG*3$>OSR4#PimKW^W1iH1HaEp@uMv7EUJg$m2c2}r z1hXIdbMriiEI1cNE9j8v{6m|WTzjR}j1C}ynsO>wuiStlHN=zt5mmmk%p=~vb zkmqEYZ^D*Idx;0d1Q(2|*kx!>ABxHq$UVHz*Xqx7R32S+C}MQHi|vgqEk3tI zHv6<8#={Mr&$~t?>5#9g%OGuAYcie;1^V>d6$Qu|%A?JhblL0}3!>}{7ox@n3v3Dt zEVj~=fX|1KY|#bb=aGr+Oy9RUJ~IneJH)GiSI{-D)G7w0PNBX5E5VwVhKgGW_!@Yk znR;1Qw>G+3-pVeJCxii1!{v&~Xyx;-$j4H5;}@!x9Dlx?LQbGegm8z#Z5#KCjZ5Of zs$uwa!uA1MGGPf>HKlje_jkcq(Q{`XhKBq?eid@FI7J+5vHR`C#iHxehbA@+ui|Na z=t?45h#at-Ow7Cy+(3p;iZi-jKMfV*<`?&IzFNmpc&OPAMZc(&ms!?X@b1tkf&p7EypcRSLdCgD;w7*OSE}LtJ1FsYe?JZ3u3{-|b1#~b~&>3owlWb1?LR;pw$f8po;-ap!3t|T zC=yLH|KXX>y6T3*ltN-<2Ph6k9sU(@|4X0vPkF1rTrFr0T#%$=cG+Le7iD|;%N%PH+Ol1+p}-HnPf>3Bv`TL67-HV_7(DLAiz0s(7(q`t-ixU^ zZ5cocF|$8rOC*4X#3f`flxPj@jGy|-@#_#LPzl=s^A%;GO7%e;|OEC zbE&%~IF=`_B_JR`4YNomiG~4o9Rkb!)W;Kc14ezlWK3*H6rN-SI00Vf3K)?PhaBOQ;2wD;Pk0(5YboNR!qL+D|!$ynJl@US26wUq>{a=6LUjSEq2Z- zwXDUXM3iSofOYpwPIX==KpZBRzT1w2<$5+KZ3h$iYtyMfG71CHp$cW{=*m1EC|?a2 zAH9d;F4IN%*pV;F0@~av*xQ0G;4(bDpU8uhBv&M`$^xGy<(Yvm5xm}-s_YymWJ&sg z$n^Y!W_aPTC&`Z-`iSD&D1&T))ps7ZRIJ)4VU-WwiaNpfriahIZ+4g zC$S?5IS~Epo*jJc*Me2jF(mnR1}uK&Zs?H@egIKnOs&v+nsje+6-6%>fH)oF7AVqd z&~evILG|@l?~>jBxU4EvmbrbHAyJJ^4yrSsD~5G7&`Qe0AZTSbS#$5!fCW?(aVZal zwc|X;%Xa?Q2J`%}Mq?c%Bip1UYbj0XNH}Lvqpd~EDG9Juf^}J0UY?qON`Jb^rC#n= zGxO&nkb-1ixaRj^x|T}s{9jD#(z(&)E!9NR(?J`eA__Y&^hU=hi4}yNR0q-Did50+ zH(PC0RNvmEm=kJ9YKIa^jpGYK`DU7~64pZ1|8r-XRJsYplLe2ZpYg%bjNX-a;o7}# z&x~bJVtb`IY^-S#>r>R6qPf&}`R6DucjXTh4U3mMB4L_>|E=5S$<3r!xeeK+(;g0C zp?yfQLX#sriE7{ExMl_t>#H)|PF34VhbN<=1FZr9l}ok93Qe@}7`J$CttH%Ao5}@- z)gK{SOm-M%Nu4?|(gqR(+39oZC9g^gmpYHc`^HG$(#y&TVnhaswd=w*tI{AAW^SB$ zl_+Xv7hPh zTm>%MK&0YFz%c2!o8h`jy=r}t@djelR0Uz3mH%aHiJ#-+SVx$fz=O__?R=jpL7ys$ zTGn85RhscLnX+Adt@;&nJdm_)(UjB8&qAWEL@F1gv-bNAl7B3q4gxtm>YYJD#SJN9va3%5&|5O?YQZPLtPBw8!+xp=* z+$(>qEDs{@sT&$SSSi(xb>LU?4A3TDTn`lpT38a9z72R6%=WFn5&SVJ!E*zziG?&# zNF>o&TO*AEIvRcri~Ji*bezdy`LUv|z}`@9DStl0@W?Zvoprc9ME+VOl3jyFkR2=w zaNqUe+_05=e;154-{I+g&=1hrTg=a8@N)dxxbQL|{L1|cp(Rtatmi8zy61*%+_?ES zEw`3v9%v0&VVJ$tp|sqnoaDx|(acopv4hpCfRgTbx%8zq;%1Bu5sJ8{GHWq&vWB~= z-evM4)IOlA)irhIzMS@cUSX%%^P_UM9EAH=6KLg9Wrxb=E)J5Lc@~F0o1LwkAN>IDqh zXC|JKTXDyCj#8nSxbcY&}a3uo}uXiN#R6G`OBh#zSH1s;_+?b zCD|{^HyI#+Pwv8U59GuJT#e-^`(pbe?W-x*EsRAxkp-33P~IVmULZG6newS zB(!U`sg{{L`|FQ-i#)W$k&)?_t){nE?ZHp)G2i5RW5PC ziQ2L&LL3di&uLmfVFB#~xF$r?UOFP=%XqT|D$4cNZH5H%M3GFaPP|cw-P|_K5_N9m zWILpBjS4fxY|^H@xcoNRZV@`Bp$RdZX)6z54fq8n5x-;mLJcjRFvEmU?+z*$BRuOM7HL9xV z*dzguPmPt>Pe_P#TwDVoc$bkUpy{4;oro?VNPr?*-oD$F-yVK+plj-!scI;UKKc@m zLH3^y0JG7#7FaMArROr-d#$X{l}MhkSvNlb))j&6G|yNyAzE+jp1~vb>)vWMWyZW`b3m zw?{L*D&DhnI@sjcByl{Nzv?~lA~Y{IV# z-pPOXsrT0syly;4anij7n>6COJ7LPOd3&$`&!X2D?)`ouIb8hFyJCH_ayjkvx1NPF ze{AvJD??EmK1M3_OM8HWjKN*^VC!7x!2#AyXEp1h3|qgP160ruJt3TM-sVDLi@<$+ zt}!aAdx@)1n<=fSjMJ6(C?N56v)ZUzTbA7`mZR~6X`IwQh&QT#DgT}rVRy-Ctcrtm z>jaMER&st>K(DZGb%_>^XxqCJ9^S|tCVGMkTuUXVnozj?^OOqbYCCq-L~Lp zF9kChBkCsLlM6gzu_8ixPxR`sO4W*m`T_V;>I5~dezkBMKm)iM2T%}zhm!gxshU(xOy{1)co40q#{N`sLMsDMxOlmvbB4UvaEmycK$BBRbr{~XJ zqMUt8m_G`U6_`O7`7v>kTE`-r3zeAc5mX>4S;u@T76ikl4PgNYwWyzS2?@gSUkDb# z-VU(JOM ztkyffjOk?kzUUP_a;CoRogFBnLj$m!#{(Kp`#C$OuHJ$kP*YHOn@ThCX#Z8}w|_p- z!dB_ud5)3h|B(af*;t$NU&Ntgbx_Pw3JgsB!Sua!&je_hu)4oS!b+WO zr{k7uDButc@5(qbV=wRF#`iIXGR8zWQ_n#bx$4hnrQxRGU z2ODWswIxQsRZ-%ld`5+sHR`anN4!ouzD+WW57VyNF~m-9TE=YeMT)C2v`LlUo5o$2 z&;4HgkUZIq>X4Oulsgy^HU%m=Ei`3$o}9N(3 z(iJimQ)!CLBi5T_l>VX3new3A+`|OvEpP#nV3x0=2oaWrid76~W+Nm?o1 zxB%M@s%6?!>>kFxiKiAd_yr_wf8l21q+oLz7NyS8OmsJn zAeixN>tO%$TBTT&z*zl5vg;`SzM#Z(OBnGje#_|XkkI~gfwKkoqO|adAla2K1#*6* zmE-qeXi6DhX2jcpj7{`2o7Cs7t!hKYf{Y}CUhRc>rww>0zurUsUthl&Kjrdrqr9dB zo}qkj>5j$WRaFsFtVb0A%lgma6;&iAhNX7PE^WyZWp!1dLt{rhQ&I|_Q?$x9(aMk6 z?NLYA|5Jkh|XvdLc2nQ&>l3Btmc`7mQNVuaaxul@C+$K0O4 z@Gye|ICZLV7tw>_d)^dJ0pIRWD+Lnfqo;>1nYnw9I}yzIAEq#iaVN**FWd3=vB@lS zPL?vQ%!R&Q&0XWpv>_a{1Y8pyt;y*u;Sn`@47FT@AIvXXIkqao!|6jpSQJY&m)g)d zVw7TkBEr?axZ1gZ~GvNgaM&ND8M~NX~?hj1JLw`o%G6v zZd>easRy!@djF$|&JaoWy(ru`%UT(Otms3s_S(`KkhDsx9-C45-!^_`n*ssN_F#S1 z@UXq8l^MM>z1EUWqtefCR(y`9S=UlQAT;4lhmEmWoZ|nKjMfyn9kE$`Gw)ntWs;e9 zymkouFyx*t3TJ@e)t3G!oms5dWESTEW;+pDEF}(-Zq?b<5*IA;s)39%v0nTaiKR`U zp|2OYP53re%+5hm>UdATT!!lQ2>d<8DVuIwmc zsw}_-jLDeO+DD+v6-GkREXGi*T5=}vmU=C)V=SRD=1-(<(mDa_$StYw!|X-!c3pgm z#PEhsYhhg2wklr=G4cDA0RdLdt=!wPKO}E(h8XE{zE%;8`+xomLrJq;9a_PHLb>^ar5n0v2-N|O@fC(`< zS(}o2uv8%Em-MCCe_PSr+bfw}s>tmT%~IG=&V3*2aD9{A)6-- zyKE4xt#NIQsZ)9%I+3P-Xgq-+!1pE4okhsHbMaY1~ja zrOn1*(VyJ}!N$d%Q4%68gUXJ1Q3b!4{gLNduqC;-oWmL5J*bJ?uCC69$>rolR!Ceg zQ%+v)|AjF==`s;}hT z%c;4Fdr-|c**Wo)Ik);rH?_+$frdnM0EMNw;p!|jp8G^^A(cZEP|P!WI=cHn;i`2L zW8e@*F{=-A2i{QWYT9cK&XFT%f?ZvRhf`T^AiZa@9SFVNF&2N=KJze(QI&DPu-3GJ zO+>LdhnMOLGSEI={}<;FZ_<_L3Cn6L760n$x2=|h*2Ni-W^1)!W(ny6t(p=cG%CE3 z0_W7bRLQ2Xq_e`vG?qp!#iv}L|;$L>ad*o^hk zp}-|BAxSVY1QI6RHJ`>25g9)I^gC4h2TmB9g5uy zcZ%rvGDdQNej7%4RM~i#%vx_7-5orP6}3M2&ChMnJz;G*swX@(xVX7cX;&Xv(SnV^eV=F;d92_nGI`*{|_>N=SF{cqO(S+BuQnfCY_@&Z)F zE_|P?tpvq7ONB@UDM0Bkxx+{&@%ffj-T=|?`0!JAQW5IIK^l^1wiei*H$;h%JZfyA z%*v(eYIZAb`>=F5Aq#JQzesKD9O8AiAOpYcrTTgxK`Ao|;_`KL1*=lBv>f+Q=Lg9s zG_?3*o<1{0x}32yUr#Frmh!;3W$$7YInk!C>auvt@0yYxxJmZxH(w2YTRgBs4Nvh} z>sC7e4YXGCSH^03gkp{MHz?AHD8^r_v9oz@Yj$l0+Q702AM&I=oNxereO;8WEKl=s z7XPYB#$QZx#7D8S8%e2xM{`kBSeZ82Tka4-0#hZBbmH%&u(W9Q2XvCGmSosYa}vJL z!XN%5RMkK?Xj`2Xd+FrqU_?=`$1udwJ_c754a;_LN({sMMAfinlw&U?Cpsf!Nn2ab zf@`v~-|4fRd0RwgZm0I;Gw<)QRWtqi5;RXf-5k}w9vl`Zg3SCSu=GEAtF!wVwURFH zD%uDT*2-Q6Fr+mK$LV2~D$!L7m?iA*=p2;THO{#HRkHd2tM06V;`+L6-vkl}e*%Hv z)&zIg-~?zIcXxMpCjKtto?^r^bH&R1X6Id$s3-IrChYQOG1 z_gr(V@f##|ZHeX*gyxz-1vjLSsvm|pYWz5U0aAM1%U%AZp*k;7Ejv?@wxG;x8kY zF~c%q)D_2sb%Ghy)pbk~ea7p(6=!)#jiKn}H-BzeBAwWPGDff`P&VB%?c$2S`mCgH z^I0@p*C@1k$eo_t-ET(F(Yv$Ey6Cx2MLL<%TvDUd4SQM&(6;w<=U!=niFTEpu>}JV zK%g%LR`w%0Ml3CUjxSC|XG(kpLxc_9!N%b)^OGH{YEN1+Ny__t<{kFOJPy;Xj$TIH zGRTDR8c8wU?F6)8NXG}ly*;aUc~faJDecaFW+&=d%gxt3QNl&x0uOn!Z84NSwDx>X`u^k4G`U&kB_+VZoqDPkQ^jB zpaid=&4+=#Sqd~)#7OX5wX;o|$)jfwDDdCxdLtFAMw}BM7*MI#I1q24uTpj8fqqZv z^5QfXq3G8r|BIsk>R%f7n~Fn2_v--goEN_R+Z<3dGUf>-{OK`}$E=_!MIDlnvf>9C z({yWU2sT@#xY~+um(siTE98vfBP1&CiR{92!~gD4ZByR9p~0zG-Y>aJZtolVVkbDu zru#O1L?YGrd%#I(fNM#>!>7cgWouOL2OFkj3Qao(YEb&l(@+w1YQ9;HNPz8&$JE$t-kUCC9z{EQb;`Yp+lp?BBCc zp=0F7qGhE^sPvvGCIQuyA%&k2uLWPY^&Xl}WsUmrm#4^eaP%m>Grz0bO~q4?MHnTg z_fvkYTjEXmOH6wlQ%1FZ@RbSF>ij6U8Jt!V- z*shbAUGh(G;i)8PAVS*=DYy1`Exn?|Bln|6uY-S&Yy@T%L!4ai!ti)cHQOa`G3r1H zB4?9t9&~B-Dvl#XQs#x(BY{gV4g4~fei`l{Qb~q+$S{E2y@*R3XN;h!Ob#QC?>SM_ zDzDUcMVfY3zwaw?ukY<9OH8iPhD$*xnHDvi9=;~t)S6|;3?aTu)%7y8<+EL&+v052 zoob;aUmv_&v&9OMHWKWK9msLKueT2$C#FRzSpSK(oMeK6$y%4++4p*=>yf+rcDb1VW0 zw6DWrG6T~s;30EyzY^@T6~bQi5vV*!_Ge?E%y4U#!Vo1M@^u|ng^uf$jO>zYC1#}p z6%U%pXqO!vOv1S8dC=P6$arBh3o!28*%T&&q~1McKUI^u>spLp&EXtWyq~M#0=BmU zxcRAKd%xa}qM@@3luV`L-efU4zmuMH-}y|c!d(Nhz^lp)zZUZ#PiHfsYQRK{_0@HX z+UFcm+#nj)wsWk9*mB$Y450hC8*-pS+4bpEtMum{i)T^@0FK3AdX1$}f0%4-wF$cMze`+r9t`uDl7jpVc zF#;(bHyJ&d$KMYC+*TcAQZlJh3cdo?LixvDEK^nn(SyC=rH}=061|+x0GlBcvE5a& z3UBq*Ri-Iu7Y06{b$v;lx25d04n@cxt&1PR5lu37X-RrNHL=s2j7%;xn&|H{G)Y%|8rR#vMFx#!)I%X2L%suc`B?SI6UnHR9Ur@4ckEz1opI zHxDegwT^8sO4gWuR<|1e1x(D_RkU-$iao5jJ|-(A!uv$LUDg<0G>3v1P3$VC-^?cj zvtwTT7M0y<-iv!@XyktZ3kB}aO2fvOym>*o+2LZNF&I{& zv6d`4%O4a`F#aSmacnEP$rJ^3_LB=f+m7-S*^uL!Kc7166wtJYFj)UO+dGAeGKPED zEpsz36ER~<$q=E2-l_SRFmKlddk;?)8#%p>TNny#gpQC&{MdTfje!OcKhml$HA4&u zuG;#}-^m0n=;?iBqpSpuK(#49rqDTE7qNcS8UCh4QJY3KX{4i=ZiBZko3+XKCy{?<9{ZLhO`PmW@BuS>!*9t{ci|I% zf(i!V#i+qd#jTn`NsRf=Ql(61ayh3#3HZfTH(7oeywpoUdloC3)g}gSt~2tYVIg6bplEQS&oM!>cbzd<>6l!)V_=rkiu~1?v*x0taccRKogwG(%%sSE zO&2p8MxivYGa{*A!;n!{*z&6X&9Nvps^>Q=V3=C%C$^^t=4rgz%hN8t4BOOsVv9Q{ zRaaf;nTSD5-arqB>O+w7iU9!M;@wke=sj_=-bmgZq~_;R&lvL%&w{DWBqd2vV@Eo< zT_qG-j)O2J2 z;p^(%{rtre+b91X%f`jywaY~C@0G6n1)6XhYzq%0B>pME5FK~j^FjyW#yHxTKVAqD zV0^KJhXV9=tRv0XECD??FuXZ0xi+3Y)p4NyP3fhTPE*vPVncH#+e*?pTLk%bE1h5@ zoUCI`7^qH;Yr|kLnf-owik}B|80uzIS(6WU@XSv#qo2UapAzw~n7xUWe%T>(*a{~s zU_r!`&NP}ElRl6>YDRM<-z%!8ZgR$$oo)&xE>VG*3n&-yRGA3%7NT# zNfLiorv5dRkld(i^QP7x$DC7!&~Xpwa#f+}LsI&Tza6cws8Q6JJ?VTwMb4G$qN-Sq z*QyLKcFp&_)L+%>REA)@f(&gMr3zp}Kq|MqU%)RwL&k!XydLulxgs7dP6m)#ysv(K zYLNvsAU{|PH>&&RhCFNFZJ;Q+!4KT#HIofcr!XnzV*Dbuhh7-Xz!6npncd<|%Y|7c|IR2P$&Va15v; z880VWF>C$5E;VyXU6+M(8&;CN@jI_+Zt{AQTHsA2E|&k=9$h+9#SBk-;LK=bu3G z?f8h+L+r=+$M|bJS{lVsIN1M%-`((RTZSTzFooHrOiJZ9s;z1iQ!Auh;G-0-rWhiL z73sem;(&kKPOJTBhMrn9c12oz5f9gVDO`FwIgD%hy*iZ*Ely3+snUUA9P_;r&RXu- zsx3O|+OHyLp4D5IOqSTih~s=G?t$I>$-GJb1ab0!+=k=qaD9|IoL<9znMb7facbE| zptwwm+-3cGSJasqmrCRyB0V0vVGS1UIh!`S^FI-3Zu8DawP2tCoZG(`ZT_jDNg0dG z;UD7zSm%sY_0(jCCzE89{e&t#T7DRdp<$mkSD!7 ztZ3TL^8o^>skeg7Iq{BZhB{ z;59R=b~W>Fhvqs2d$446iBL?G!b!*@nyAb|kACxCY?@I+FVZGxKR)hS_mOw17k9V$ zxV;airq%c95@ocYX$QYgl%Cad6fSSevY7BceZg*ge73Vlwl{LcEEAPEP^X?~!I`Mv ztvl5dE^ky}NH&(Rm`*W9y32HKK2ofnL{yhI9rZIM<|rU~5K3U< zXq+LRq3=Y85)g1QiDSOx^-#{i;6_8bnxxK}Dfu$li%e%f=?jI&Z5XZszf9}-z+aStU3PN!MdyXPKu?lwh zwO*nxcxvWy2Wv}2C0NDl2)34LU8R*J*YrZuShIgCXqKviW0CGGySrp8pE3o>e{(ST zLNc@MQ6SRv%|0SaRTSN@AEYtbx&{V1WBK}tq#;pI>lsm4C2ZzMyukt1v)`+Ko7dq=^j|}S6!kspQL8<+TE2x z`kGh4tDOVWSBfxz4656Q%=2+?>W>(0LmaU3(bz0~`=s^Mzkp;?%e1Ib(w7|${N2Vk zFHIj(>jU8qt;N5iAOGP8C3*q(m7STQ&qV0af>4Trg8|zLI-R&uXjR=t;zL2i(%u*^ zlxIdLe*qk$z*w21l~uTP)oI1gBi#7EfC&8`Fz;N)K*RapiI&XNg3(@w3q^{#vzWXP z^6u;9D%r5DEw?bzc`V4ltX)BXRBQ~18ah~QNU55Qsi?GnqLf5Ta}Vi?yMAe$+E`zZ z5vx6Yip!uRd*)NCO$$T$e5@DVOkva`2?)jb4H#WUiHNLxk3C~s!w?M2@#X$iVGj06 zoE9>k>+m+@q2<~MIaonme}JY=j+eyJ;C8(mpy!LtKD(_oja29O37^6$D^@>G!~=6z z*;du)Z!ez5`!35}+x?a$yn1WYE@;)mtq}6jV-0uj^^`Ni*k=k;A>n~neh*0CaooXG zcPqjn7cjW!{mvSf-nlY*$SM#9tC6@%uBSSSfkhpxnB(+M-(c0t*8(v zCfg0syh3THOg{uQ8Qmf5qd4JOcy-q@RBu8g}mGRqm6?i?|kvA_m=wW4j;ejiMTHB<#6qt!}1 zOXO{~?~vK@!lU>>Z~y2!+sB(kPfdS6pWY|+Ue+NJZwvUdl|*c4#sJI?f0dy>9XiUB zY!GFOiN7iQCBe3%?3jtT?wf02%$#H4B0m3D4v|B;>6SZCljgY-#N@UkW~VTxZX>N8 zk=W#rt8Z(_y32+!{BaW0S%F;G4JPY$4;$$1j3S^{L?VTjg4_{F5vuFoKCVD!$YvFf z5;EYobSniFZ8~hHEp)`Y^>+E6yl>u=KO(e~7tYtu+sH^=$--A|(_<~%Qj+RY=oRb$ zLV%hufkLRv9Ls5S#QBa`8x=&PD$uG+wB{L)akntuqh{0|p+jeXLoEMST=;K>Tm@0| zD2L&K-^|0!u}GsdQm)W=qW#71Eba4?9>wGAvRwNE%1Dah=`KE=_pgx$dNj|dEUVk= zCU$wswlw<3MANjUaJGek4W$==TTk7M2ZAMLZ9a<5GQ)V+Y&zGPIZ()cjLYg~Ic9Rj z^NuhnRwvVk~E-P(Bgo?toAldBn@zPV9Qd?R+ik$uv zHO-X}d-J5%YTs#BT@vg`qVDVio<-4p&8^RNm}Q8z9^)_c58sN_L(L5fv+37J-lP#Q zC7{88N9refL-Lg(`>0<6Y_5I3oev8<_Iyg)pQXG5kFEoqQGZNI8|nqS=7OealcKgR z2$AVp&zT}?%^y2BgCBrhH)Z?Wle=%ehTmOgXT#xx{Nj&wkgFEjR;*os`HGD4TjVcx zu@iBAcn>C(paR9gJmGP#5>=^;e$Qbi+vJF!x$MeTM__J2%Mk%1eV}Zdvfpwl&u?oJ z99BfL(l~2}xiEI6em*=_Y#OE?(f76kBXl;QHB3{QQJn0UL2B@H9V`+;=e_;Bgz?!b z*ZV~cv|<|HNE*a?et20^pwfsmUE(t6tIAvx_!qzwy0oBPeEo@<8y=bP=Bn^#)R#GP$G02R0@K`Eb*^dZLVJ3{9+t(c%$xt!9wD>1{Yh(%WN0g<>NU!->B{ode zvMnDqO2rFC-sC~KLc&-yW%h@9uh_SekMx~90>)%ezc6T7L}Kcp z?q`AqUW;u%k_L4N9NAeCG-Q?daJ2yhNc8R%=p(6{abz&^KN(%KlqxBg<7zNL`B(}a?MTAZ}75mZb?wy*V zB5VSh!_xd+VFO$eKQ$=IcLnyv3uI{0ufT@GxzOa}?*z9MvJbgC*@J!<(O-?n5S;m+ z<)W64Ne`cznkD;_TLGvW2-hx`tU*~E&rf8^H(NonR}^juF;OnV+Z&Kb&s=GpfXbjj z7AnohrJs10dyhqA9npR-xce-x?!NL$OS9XY;)9kfNMiidrk5+*Ix%r~0f${wa%nc6 zHcUys!y*)I&<@9MyTfz!jz(S;or)_v+gfhSJaOvv5Z;^S1Z3zD;W2YGtx`h7fn-Us zYc5axr0BP&0{o1ewhojlU$f0`2j9?)3~IXxA4H!rCqlCajbv1d0-WGcDPZQP<%ah;m;ezT^0mve$<1%jqXWucP z`|ZwVHLhPu8oOFjoFT_tDx5pMm2ma)!_u`7UdgBO5}7A)^EUW~0tisBN|RRi6wbVp z7I3sBP;G};FSj_1P+%v%L)lgd6o;lL3q_oHNLz@RL*}@3;ySAk>lC-&K7Uh z0`T_)2lqrTCahu>DV`tZ0iZwWb-4!kdz>81&9IyfrF<>2;nAK?6E^{@7l;&c+xK!P zW7>C;W>oR))CoZti}SeS>T}FZc4|)Lx`S?S56z>6aZq_d0op=^+6jB{F1xxzWcdp++>&kvx9LMhc;4-N^Z2OJ?;hoN(j@$msX3e-HeT zSxMmb9;*AxytN4jkJE=_KKQ0MX=CsEdcBwuzoA{UTw?+Q)sLGCHT?4q%(a-O$N6+| zQ7IpVT65zGl^;CX{gfx(I(J0~+)yuWKoDL1W}+9d@k~ z_ZZN4^d)6wXG(*lCo;fxsrk5qHw0Ex++t7)a9LB;ev~%zQFh@Rauw>n8PMA$8l?_c za~@aostZOUQju>3Y?&v9Uf)*K5C=faSPIjZApP4(9cW|y{X%NrzJwt;cXWij*bKNx zYG6ft8DGoO9joTj62>Ufk$7`1NUD{fCzHRmADGI4SKk?eOwZ2(5pt0mwj<)@iE0kY z=ajdtYJu&nj&d0y~*>Aa(D`6igpD`tw6sE6;xs*w9gOH7x2!3T?l=*DBu^ z=rc=RDN>i5*P|3}T!I_dUU{4+C8C#M) zXzI0BH+qlgHD7^-^9T7c_)}L2 z<8fNTT}O0Tc^C4IR%kI^0HwBSsJtJ!P`*{(Sf+w|Wk?>!b=kd>khJ;dup8U>)=muy zrbC{>B1W=0aiaWf4lD=XHr@InB+LIQl5>Q<>!JtUr|piNE;LjCn5g_>}~ea@B6G zff+?M&F3?X#Ri6B)|~H0cCqakXaAaz5)=sRgtO2LnX=dU-v2Ok;` zy%m07MjMHjBfgt&(8^!HA*^aqWww?(dd8t#XVlKMI)vnM$MZDkn}1(zRe4Easm`(H z(8kLO(>Epe#LrmpiUm`Nz-XR*Sz)gI5N9eRYNdBy!c~RE5BY%yX^qaGUw_>9M^Os2 zGk|vE*v!gY*|fOo;S54$aw$-MSy5~SdVD@651GHFwkeLaYLU;w$7AI<4@wE}zRx_= zi=f$vP8f}8r>wrfXUEeRejbjmme)BZHx9<}#?x@UbRDR0>^&sC9C^?(vi6=K?8zAN zp@ykv)4Up1j0LO3_Kja?6+lcGR<+`xJu24fyUUh%o!jdHL ze2silH-;34fV?x|%`JTBHXSmD~bN5)RQzePNS-X0%>z{`~1$N27j# ziw0g(yj`=*Xw@XSrxx#exqt(oT}(P67ic#Sv@Hm(C4P{_rfPdgaAKPCdzuZ znD^qv_{#j()!#OT(c6Zn!d0sXiLV%byd5rfwivq|9&4zkLN#f@SQLW`1>%OX551)T z<{&}0NH3#iPDg&fn;bppqMBU8mTBBFenpQHn(Mc4{6+v1PiVmGi$H$4ev(bc+H1Nd z??rF|pZUTXc5Gbwy)+ij?}Rdhznj=}5gd$EM1J`ySly65uiHp3meA(7SM(l$7(F zMcN&cI6qs(BduAKfp9xD>sZZ)3MeZMBignu&Q@qaoDc`hwDPv)3*qP*dfeK>1Jpfj zk}BAI1&a~sX3}O#K9h%9!O+%$R?@%p2PxD|zXN}x->Jl=CbxcVqE0U|*M_iy5(Dyb zQ{rCQ-?pE8W+2|of4}M&G12=__W~>pDz+qC(4UxEIhG<(ih5LYLZ4l*OTHk6Je^4V z5+Ul_rfy%G<&GWK8xlj_(_8#gjjS+t#yxK0WP;)>A+QokByza+bhPhb)Av;{&HlTk zdNI_O$R^VOl0gt=;(+`1q|R9hddm%uVmt=BM^#@esYv1dyC3D>>?!~It?O}3vGyBP zcJUj$dED26WiLSM_Bp3znw0KpyQa?p>R*_+j+MgO5AuX!P4k@Uk)1jCvfcgyW<~$g z>v5;~DE0Z%M^S4Cfg*W)VkgmVo42KE64mPzZbR$Kb1bI|xjyGM{^+8MBmY+nbTfFhUNh`0k#oY&G^O70!j?|g^?4INj z#+9C8hM4x3duX(M9jLdgP=L0&4${(ew-LTEK&P_du~MGR@MmyIZxRVlk0R&=d%XT| z-TCd!DI=z0@-bRI*JQzFm08mC2NSIW(Ul4%OuK4)vqV(5-|M}Mm?6dIC4H|+#uh9> z#^sSAP?u!-`R-EEn|NmJLI!E0-Z-YnJ;vh+FQDVSNM}^drFmp(IfQTzM?#yk!C`8i zBJmD0h*-gqgXrpy^xG*>Oh&A;qymb!>7nQPzObci|1KNN&NW;)x^y3Z8ls0l(p)AW zGU*Ht-XX%5#M+@nmmmr+o;~JI5%Mz_9`ETEKN}Pun;=8!9Xh0xr0+}h!c;sz%sgY& zB>9$M4(1KUl^aqNEp5WX5Gd%!FXyR~vIhu@i>3+@Z=sb28g*C%UWis}USA3w3-QJl zS&ut>PVPJOYIa#k;$+-4-(N9&Om?~qV*3k7n=MkGq@>*%S3L>mL8Z~?@IN~1?jWHj zfyLd(wuB$-@K`(0wlNRt{R~S-{>_Iqiy#IY*dFzU%5`Gwz3N70jGuVdS~XOuRb;~! zQ@>_cH>A>#uuS--gLq89pb7wgQ(1pqsxq)UI9qFwcm8e2<=!&ehKvrXHOd({*dCHR zQR1ZyeG2y}4Lyd@k z99_L9w&yl*y8Wp@`AjBMg}hKO*p_IK`s- zGZC=bzO3n)XkBiViq$u#ljr?+PMlYv$4vbd`W?*1fw0*AR@B9oVqN#mFx`}eu=84$ zGxJ3M%El43JwM|Y+Ek^m%o;p}vjkVL(%IcA%81YW@q?(~py5TI&4@}dPu2kGPSn{*d+$Z$L z!qJTP-C6NGbW(Vqbg#uWo)MW;I{$;<@^AXf|H~F_ZDn;WUoYJ>18D?@!X$4fUH|$# z)+4^rMiU2?C)`dR#F$!8H^?PuU9IoVCcnlM$?d1o(>=)W{7zE;iTNnQoH=1#EI>#w zd>1xpL|<*-5+pq~%GSUGL~C>?0tc0M2gsGUAi(s++=v*{06rW0gs#f9v(qaX_1)F+ zm=_+;f*%!M9yrgk9vOrF0&0D=oT+4@dm{f+ diff --git a/public/assets/angular-material-assets/img/icons/android.svg b/public/assets/angular-material-assets/img/icons/android.svg new file mode 100644 index 0000000..306b4e1 --- /dev/null +++ b/public/assets/angular-material-assets/img/icons/android.svg @@ -0,0 +1 @@ + diff --git a/public/assets/angular-material-assets/img/icons/angular-logo.svg b/public/assets/angular-material-assets/img/icons/angular-logo.svg new file mode 100644 index 0000000..513382a --- /dev/null +++ b/public/assets/angular-material-assets/img/icons/angular-logo.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/assets/angular-material-assets/img/icons/bower-logo.svg b/public/assets/angular-material-assets/img/icons/bower-logo.svg new file mode 100644 index 0000000..e78c9c1 --- /dev/null +++ b/public/assets/angular-material-assets/img/icons/bower-logo.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/assets/angular-material-assets/img/icons/cake.svg b/public/assets/angular-material-assets/img/icons/cake.svg new file mode 100644 index 0000000..512fae0 --- /dev/null +++ b/public/assets/angular-material-assets/img/icons/cake.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/angular-material-assets/img/icons/codepen-logo.svg b/public/assets/angular-material-assets/img/icons/codepen-logo.svg new file mode 100644 index 0000000..c26d0b0 --- /dev/null +++ b/public/assets/angular-material-assets/img/icons/codepen-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/angular-material-assets/img/icons/copy.svg b/public/assets/angular-material-assets/img/icons/copy.svg new file mode 100644 index 0000000..51ab2e6 --- /dev/null +++ b/public/assets/angular-material-assets/img/icons/copy.svg @@ -0,0 +1 @@ + diff --git a/public/assets/angular-material-assets/img/icons/copy2.svg b/public/assets/angular-material-assets/img/icons/copy2.svg new file mode 100644 index 0000000..8609b58 --- /dev/null +++ b/public/assets/angular-material-assets/img/icons/copy2.svg @@ -0,0 +1 @@ + diff --git a/public/assets/angular-material-assets/img/icons/facebook.svg b/public/assets/angular-material-assets/img/icons/facebook.svg new file mode 100644 index 0000000..a18345d --- /dev/null +++ b/public/assets/angular-material-assets/img/icons/facebook.svg @@ -0,0 +1 @@ + diff --git a/public/assets/angular-material-assets/img/icons/favorite.svg b/public/assets/angular-material-assets/img/icons/favorite.svg new file mode 100644 index 0000000..a54d02d --- /dev/null +++ b/public/assets/angular-material-assets/img/icons/favorite.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/assets/angular-material-assets/img/icons/github-icon.svg b/public/assets/angular-material-assets/img/icons/github-icon.svg new file mode 100644 index 0000000..609b349 --- /dev/null +++ b/public/assets/angular-material-assets/img/icons/github-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/angular-material-assets/img/icons/github.svg b/public/assets/angular-material-assets/img/icons/github.svg new file mode 100644 index 0000000..71faa13 --- /dev/null +++ b/public/assets/angular-material-assets/img/icons/github.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/assets/angular-material-assets/img/icons/hangout.svg b/public/assets/angular-material-assets/img/icons/hangout.svg new file mode 100644 index 0000000..d16fde7 --- /dev/null +++ b/public/assets/angular-material-assets/img/icons/hangout.svg @@ -0,0 +1 @@ + diff --git a/public/assets/angular-material-assets/img/icons/ic_access_time_24px.svg b/public/assets/angular-material-assets/img/icons/ic_access_time_24px.svg new file mode 100644 index 0000000..9d30d21 --- /dev/null +++ b/public/assets/angular-material-assets/img/icons/ic_access_time_24px.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/angular-material-assets/img/icons/ic_arrow_back_24px.svg b/public/assets/angular-material-assets/img/icons/ic_arrow_back_24px.svg new file mode 100644 index 0000000..aeb9ec3 --- /dev/null +++ b/public/assets/angular-material-assets/img/icons/ic_arrow_back_24px.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/angular-material-assets/img/icons/ic_build_24px.svg b/public/assets/angular-material-assets/img/icons/ic_build_24px.svg new file mode 100644 index 0000000..dcf349f --- /dev/null +++ b/public/assets/angular-material-assets/img/icons/ic_build_24px.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/assets/angular-material-assets/img/icons/ic_card_giftcard_24px.svg b/public/assets/angular-material-assets/img/icons/ic_card_giftcard_24px.svg new file mode 100644 index 0000000..c134fa3 --- /dev/null +++ b/public/assets/angular-material-assets/img/icons/ic_card_giftcard_24px.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/assets/angular-material-assets/img/icons/ic_chevron_right_24px.svg b/public/assets/angular-material-assets/img/icons/ic_chevron_right_24px.svg new file mode 100644 index 0000000..9d4c76c --- /dev/null +++ b/public/assets/angular-material-assets/img/icons/ic_chevron_right_24px.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/angular-material-assets/img/icons/ic_close_24px.svg b/public/assets/angular-material-assets/img/icons/ic_close_24px.svg new file mode 100644 index 0000000..865788b --- /dev/null +++ b/public/assets/angular-material-assets/img/icons/ic_close_24px.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/angular-material-assets/img/icons/ic_code_24px.svg b/public/assets/angular-material-assets/img/icons/ic_code_24px.svg new file mode 100644 index 0000000..cf4d036 --- /dev/null +++ b/public/assets/angular-material-assets/img/icons/ic_code_24px.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/assets/angular-material-assets/img/icons/ic_comment_24px.svg b/public/assets/angular-material-assets/img/icons/ic_comment_24px.svg new file mode 100644 index 0000000..ffe633e --- /dev/null +++ b/public/assets/angular-material-assets/img/icons/ic_comment_24px.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/angular-material-assets/img/icons/ic_email_24px.svg b/public/assets/angular-material-assets/img/icons/ic_email_24px.svg new file mode 100644 index 0000000..d5db160 --- /dev/null +++ b/public/assets/angular-material-assets/img/icons/ic_email_24px.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/assets/angular-material-assets/img/icons/ic_euro_24px.svg b/public/assets/angular-material-assets/img/icons/ic_euro_24px.svg new file mode 100644 index 0000000..abaadd4 --- /dev/null +++ b/public/assets/angular-material-assets/img/icons/ic_euro_24px.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/assets/angular-material-assets/img/icons/ic_insert_drive_file_24px.svg b/public/assets/angular-material-assets/img/icons/ic_insert_drive_file_24px.svg new file mode 100644 index 0000000..4c074e0 --- /dev/null +++ b/public/assets/angular-material-assets/img/icons/ic_insert_drive_file_24px.svg @@ -0,0 +1,6 @@ + + + + + diff --git a/public/assets/angular-material-assets/img/icons/ic_label_24px.svg b/public/assets/angular-material-assets/img/icons/ic_label_24px.svg new file mode 100644 index 0000000..c17cd8d --- /dev/null +++ b/public/assets/angular-material-assets/img/icons/ic_label_24px.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/angular-material-assets/img/icons/ic_launch_24px.svg b/public/assets/angular-material-assets/img/icons/ic_launch_24px.svg new file mode 100644 index 0000000..5ca5643 --- /dev/null +++ b/public/assets/angular-material-assets/img/icons/ic_launch_24px.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/angular-material-assets/img/icons/ic_menu_24px.svg b/public/assets/angular-material-assets/img/icons/ic_menu_24px.svg new file mode 100644 index 0000000..e2dd3d2 --- /dev/null +++ b/public/assets/angular-material-assets/img/icons/ic_menu_24px.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/angular-material-assets/img/icons/ic_more_vert_24px.svg b/public/assets/angular-material-assets/img/icons/ic_more_vert_24px.svg new file mode 100644 index 0000000..deb8f64 --- /dev/null +++ b/public/assets/angular-material-assets/img/icons/ic_more_vert_24px.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/angular-material-assets/img/icons/ic_ondemand_video_24px.svg b/public/assets/angular-material-assets/img/icons/ic_ondemand_video_24px.svg new file mode 100644 index 0000000..1c4c90a --- /dev/null +++ b/public/assets/angular-material-assets/img/icons/ic_ondemand_video_24px.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/assets/angular-material-assets/img/icons/ic_people_24px.svg b/public/assets/angular-material-assets/img/icons/ic_people_24px.svg new file mode 100644 index 0000000..45592be --- /dev/null +++ b/public/assets/angular-material-assets/img/icons/ic_people_24px.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/angular-material-assets/img/icons/ic_person_24px.svg b/public/assets/angular-material-assets/img/icons/ic_person_24px.svg new file mode 100644 index 0000000..6ce9d1e --- /dev/null +++ b/public/assets/angular-material-assets/img/icons/ic_person_24px.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/assets/angular-material-assets/img/icons/ic_phone_24px.svg b/public/assets/angular-material-assets/img/icons/ic_phone_24px.svg new file mode 100644 index 0000000..79abfad --- /dev/null +++ b/public/assets/angular-material-assets/img/icons/ic_phone_24px.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/assets/angular-material-assets/img/icons/ic_photo_24px.svg b/public/assets/angular-material-assets/img/icons/ic_photo_24px.svg new file mode 100644 index 0000000..771b4a1 --- /dev/null +++ b/public/assets/angular-material-assets/img/icons/ic_photo_24px.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/angular-material-assets/img/icons/ic_place_24px.svg b/public/assets/angular-material-assets/img/icons/ic_place_24px.svg new file mode 100644 index 0000000..6a441cd --- /dev/null +++ b/public/assets/angular-material-assets/img/icons/ic_place_24px.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/assets/angular-material-assets/img/icons/ic_play_arrow_24px.svg b/public/assets/angular-material-assets/img/icons/ic_play_arrow_24px.svg new file mode 100644 index 0000000..30d64fa --- /dev/null +++ b/public/assets/angular-material-assets/img/icons/ic_play_arrow_24px.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/angular-material-assets/img/icons/ic_play_circle_fill_24px.svg b/public/assets/angular-material-assets/img/icons/ic_play_circle_fill_24px.svg new file mode 100644 index 0000000..5518b34 --- /dev/null +++ b/public/assets/angular-material-assets/img/icons/ic_play_circle_fill_24px.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/assets/angular-material-assets/img/icons/ic_refresh_24px.svg b/public/assets/angular-material-assets/img/icons/ic_refresh_24px.svg new file mode 100644 index 0000000..bfd3c59 --- /dev/null +++ b/public/assets/angular-material-assets/img/icons/ic_refresh_24px.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/angular-material-assets/img/icons/ic_school_24px.svg b/public/assets/angular-material-assets/img/icons/ic_school_24px.svg new file mode 100644 index 0000000..460deb6 --- /dev/null +++ b/public/assets/angular-material-assets/img/icons/ic_school_24px.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/assets/angular-material-assets/img/icons/ic_visibility_24px.svg b/public/assets/angular-material-assets/img/icons/ic_visibility_24px.svg new file mode 100644 index 0000000..19768e3 --- /dev/null +++ b/public/assets/angular-material-assets/img/icons/ic_visibility_24px.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/angular-material-assets/img/icons/launch.svg b/public/assets/angular-material-assets/img/icons/launch.svg new file mode 100644 index 0000000..70de58e --- /dev/null +++ b/public/assets/angular-material-assets/img/icons/launch.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/assets/angular-material-assets/img/icons/list_control_down.png b/public/assets/angular-material-assets/img/icons/list_control_down.png new file mode 100644 index 0000000000000000000000000000000000000000..696604b8bda8d7e519c90f9f2d148915c8eb8b5a GIT binary patch literal 197 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZADV^Qxa(4UIJ!R(o^5O~y{x?*Iv%UQ*y<JYD@<);T3K0RRmpOc4M8 literal 0 HcmV?d00001 diff --git a/public/assets/angular-material-assets/img/icons/mail.svg b/public/assets/angular-material-assets/img/icons/mail.svg new file mode 100644 index 0000000..8a36e49 --- /dev/null +++ b/public/assets/angular-material-assets/img/icons/mail.svg @@ -0,0 +1 @@ + diff --git a/public/assets/angular-material-assets/img/icons/menu.svg b/public/assets/angular-material-assets/img/icons/menu.svg new file mode 100644 index 0000000..b2de22a --- /dev/null +++ b/public/assets/angular-material-assets/img/icons/menu.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/assets/angular-material-assets/img/icons/message.svg b/public/assets/angular-material-assets/img/icons/message.svg new file mode 100644 index 0000000..10d347d --- /dev/null +++ b/public/assets/angular-material-assets/img/icons/message.svg @@ -0,0 +1 @@ + diff --git a/public/assets/angular-material-assets/img/icons/more_vert.svg b/public/assets/angular-material-assets/img/icons/more_vert.svg new file mode 100644 index 0000000..6e1d96d --- /dev/null +++ b/public/assets/angular-material-assets/img/icons/more_vert.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/assets/angular-material-assets/img/icons/npm-logo.svg b/public/assets/angular-material-assets/img/icons/npm-logo.svg new file mode 100644 index 0000000..b0f6397 --- /dev/null +++ b/public/assets/angular-material-assets/img/icons/npm-logo.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/assets/angular-material-assets/img/icons/octicon-repo.svg b/public/assets/angular-material-assets/img/icons/octicon-repo.svg new file mode 100644 index 0000000..2bf9537 --- /dev/null +++ b/public/assets/angular-material-assets/img/icons/octicon-repo.svg @@ -0,0 +1,5 @@ + + + diff --git a/public/assets/angular-material-assets/img/icons/print.svg b/public/assets/angular-material-assets/img/icons/print.svg new file mode 100644 index 0000000..91365b2 --- /dev/null +++ b/public/assets/angular-material-assets/img/icons/print.svg @@ -0,0 +1 @@ + diff --git a/public/assets/angular-material-assets/img/icons/separator.svg b/public/assets/angular-material-assets/img/icons/separator.svg new file mode 100644 index 0000000..d8fa31d --- /dev/null +++ b/public/assets/angular-material-assets/img/icons/separator.svg @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/public/assets/angular-material-assets/img/icons/sets/communication-icons.svg b/public/assets/angular-material-assets/img/icons/sets/communication-icons.svg new file mode 100644 index 0000000..1e2fbb0 --- /dev/null +++ b/public/assets/angular-material-assets/img/icons/sets/communication-icons.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/angular-material-assets/img/icons/sets/core-icons.svg b/public/assets/angular-material-assets/img/icons/sets/core-icons.svg new file mode 100644 index 0000000..60c9e7b --- /dev/null +++ b/public/assets/angular-material-assets/img/icons/sets/core-icons.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/angular-material-assets/img/icons/sets/device-icons.svg b/public/assets/angular-material-assets/img/icons/sets/device-icons.svg new file mode 100644 index 0000000..cc5bfd7 --- /dev/null +++ b/public/assets/angular-material-assets/img/icons/sets/device-icons.svg @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/angular-material-assets/img/icons/sets/social-icons.svg b/public/assets/angular-material-assets/img/icons/sets/social-icons.svg new file mode 100644 index 0000000..3b39255 --- /dev/null +++ b/public/assets/angular-material-assets/img/icons/sets/social-icons.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/assets/angular-material-assets/img/icons/share-arrow.svg b/public/assets/angular-material-assets/img/icons/share-arrow.svg new file mode 100644 index 0000000..2d6befc --- /dev/null +++ b/public/assets/angular-material-assets/img/icons/share-arrow.svg @@ -0,0 +1 @@ + diff --git a/public/assets/angular-material-assets/img/icons/tabs-arrow.svg b/public/assets/angular-material-assets/img/icons/tabs-arrow.svg new file mode 100644 index 0000000..c7b7986 --- /dev/null +++ b/public/assets/angular-material-assets/img/icons/tabs-arrow.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + diff --git a/public/assets/angular-material-assets/img/icons/toggle-arrow.svg b/public/assets/angular-material-assets/img/icons/toggle-arrow.svg new file mode 100644 index 0000000..0c5b64d --- /dev/null +++ b/public/assets/angular-material-assets/img/icons/toggle-arrow.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/public/assets/angular-material-assets/img/icons/twitter.svg b/public/assets/angular-material-assets/img/icons/twitter.svg new file mode 100644 index 0000000..e4f85eb --- /dev/null +++ b/public/assets/angular-material-assets/img/icons/twitter.svg @@ -0,0 +1,2 @@ + diff --git a/public/assets/angular-material-assets/img/icons/upload.svg b/public/assets/angular-material-assets/img/icons/upload.svg new file mode 100644 index 0000000..6a673db --- /dev/null +++ b/public/assets/angular-material-assets/img/icons/upload.svg @@ -0,0 +1 @@ + diff --git a/public/assets/angular-material-assets/img/list/60.jpeg b/public/assets/angular-material-assets/img/list/60.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..f0acea424a7a009c4d8bd75c876c7a8cf1240a28 GIT binary patch literal 1193 zcmex=rNm5Eg zMpH#wOG8CnUDwFLT-U(XKwaI!$I{l>)x*<6+sr@I&n?8k-NTiUhnH87Ur4*?GC&JtLjzoEuWg<&y2!YcWBh`abls)oh#eJ)~6O-xTAO7xk-!p>&QaA3F%8llnK56O8 zXZZLREbz_cb7*>WP4gN{j!vuDdzN_)>gqjZ=ifDF{rF+xTy)86?FpOb_rGZSUuE$X z`+9HUfQG>B9-f z11z;G=PxhGu5}8pTzjvS;jgMn`UedY-Z+a=ZH8^G65x#^399{Wtjk-x1ywe!zB~0yPi4rr$Nw2J^Fuaon6bURmTUXMyIsXs|7qP1EZKYO z0CPJtL;qgo*J9i1MK(u^2JU_yV8H))E91Yp8ynY#PbrrXi!1i&PIGuzvD9v|<=)8$ zOK-V(RRzDYKV`AtT#{?6(|XDG$Ez;w?|a<->-O@%uaO&$-#!tR7x8|vmDtMCWv8Cq z{TKdl!gKb$%<}{4l_D~>yPf)W?rpTRCx?Oiij&V)IsR$CJgaW%T+7r{nW@UW^Q$~x zW&iGtx;X9QktQ=$yTGa6*!L=J7CqI&=y3juZD#4}*gex$KZt&JYUPtIZjo@SqlZ?h zTRuJTAbeKpobsZ#Q??o3kV%`nWnz1L8pYYdlSH3lK`k!wL Ve + + + + + + + + + + diff --git a/public/assets/angular-material-assets/img/mangues.jpg b/public/assets/angular-material-assets/img/mangues.jpg new file mode 100644 index 0000000000000000000000000000000000000000..556d8538d5c4590c3e9239f5c07dbe758cc27756 GIT binary patch literal 300522 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