diff options
author | Ed Tanous <ed.tanous@intel.com> | 2017-06-07 01:28:13 +0300 |
---|---|---|
committer | Ed Tanous <ed.tanous@intel.com> | 2017-06-07 01:28:13 +0300 |
commit | 4758d5be3debd098e3ce9ba703c75269ecf6f9b1 (patch) | |
tree | 4f899ea041a389a3e26a774ff019d4657bdcb284 | |
parent | cfbe25de0832059d2752d6cec5f1924a40f20f17 (diff) | |
download | bmcweb-4758d5be3debd098e3ce9ba703c75269ecf6f9b1.tar.xz |
incremental
34 files changed, 8532 insertions, 4807 deletions
diff --git a/.clang-format b/.clang-format index 113030c683..6f1017fdb3 100644 --- a/.clang-format +++ b/.clang-format @@ -1,6 +1,5 @@ --- -Language: Cpp -# BasedOnStyle: Google +BasedOnStyle: Google AccessModifierOffset: -1 AlignAfterOpenBracket: Align AlignConsecutiveAssignments: false diff --git a/CMakeLists.txt b/CMakeLists.txt index 1b3fa77c06..ae75e5de87 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -149,6 +149,10 @@ hunter_add_package(ZLIB) find_package(ZLIB REQUIRED) include_directories(${ZLIB_INCLUDE_DIRS}) + +# PAM +find_package(PAM REQUIRED) + set(WEBSERVER_MAIN src/webserver_main.cpp) set(HDR_FILES @@ -224,6 +228,7 @@ if(${BUILD_UT}) target_link_libraries(unittest ${OPENSSL_LIBRARIES}) target_link_libraries(unittest ${ZLIB_LIBRARIES}) + target_link_libraries(unittest pam) add_dependencies(unittest packagestaticcpp) endif(${BUILD_UT}) @@ -238,6 +243,7 @@ target_link_libraries(bmcweb g3logger) target_link_libraries(bmcweb ${OPENSSL_LIBRARIES}) target_link_libraries(bmcweb ${ZLIB_LIBRARIES}) target_link_libraries(bmcweb ${DBUS_LIBRARIES}) +target_link_libraries(bmcweb pam) add_dependencies(bmcweb packagestaticcpp) install (TARGETS bmcweb DESTINATION bin) diff --git a/cmake/FindPAM.cmake b/cmake/FindPAM.cmake new file mode 100644 index 0000000000..25307bd3c3 --- /dev/null +++ b/cmake/FindPAM.cmake @@ -0,0 +1,71 @@ +# - Try to find the PAM libraries +# Once done this will define +# +# PAM_FOUND - system has pam +# PAM_INCLUDE_DIR - the pam include directory +# PAM_LIBRARIES - libpam library + +if (PAM_INCLUDE_DIR AND PAM_LIBRARY) + # Already in cache, be silent + set(PAM_FIND_QUIETLY TRUE) +endif (PAM_INCLUDE_DIR AND PAM_LIBRARY) + +find_path(PAM_INCLUDE_DIR NAMES security/pam_appl.h pam/pam_appl.h) +find_library(PAM_LIBRARY pam) +find_library(DL_LIBRARY dl) + +if (PAM_INCLUDE_DIR AND PAM_LIBRARY) + set(PAM_FOUND TRUE) + if (DL_LIBRARY) + set(PAM_LIBRARIES ${PAM_LIBRARY} ${DL_LIBRARY}) + else (DL_LIBRARY) + set(PAM_LIBRARIES ${PAM_LIBRARY}) + endif (DL_LIBRARY) + + if (EXISTS ${PAM_INCLUDE_DIR}/pam/pam_appl.h) + # darwin claims to be something special + set(HAVE_PAM_PAM_APPL_H 1) + endif (EXISTS ${PAM_INCLUDE_DIR}/pam/pam_appl.h) + + if (NOT DEFINED PAM_MESSAGE_CONST) + include(CheckCXXSourceCompiles) + # XXX does this work with plain c? + check_cxx_source_compiles(" +#if ${HAVE_PAM_PAM_APPL_H}+0 +# include <pam/pam_appl.h> +#else +# include <security/pam_appl.h> +#endif +static int PAM_conv( + int num_msg, + const struct pam_message **msg, /* this is the culprit */ + struct pam_response **resp, + void *ctx) +{ + return 0; +} +int main(void) +{ + struct pam_conv PAM_conversation = { + &PAM_conv, /* this bombs out if the above does not match */ + 0 + }; + return 0; +} +" PAM_MESSAGE_CONST) + endif (NOT DEFINED PAM_MESSAGE_CONST) + set(PAM_MESSAGE_CONST ${PAM_MESSAGE_CONST} CACHE BOOL "PAM expects a conversation function with const pam_message") + +endif (PAM_INCLUDE_DIR AND PAM_LIBRARY) + +if (PAM_FOUND) + if (NOT PAM_FIND_QUIETLY) + message(STATUS "Found PAM: ${PAM_LIBRARIES}") + endif (NOT PAM_FIND_QUIETLY) +else (PAM_FOUND) + if (PAM_FIND_REQUIRED) + message(FATAL_ERROR "PAM was not found") + endif(PAM_FIND_REQUIRED) +endif (PAM_FOUND) + +mark_as_advanced(PAM_INCLUDE_DIR PAM_LIBRARY DL_LIBRARY PAM_MESSAGE_CONST) diff --git a/crow/include/crow/http_connection.h b/crow/include/crow/http_connection.h index 09a1454f78..af18541f95 100644 --- a/crow/include/crow/http_connection.h +++ b/crow/include/crow/http_connection.h @@ -27,33 +27,29 @@ using tcp = asio::ip::tcp; namespace detail { template <typename MW> struct check_before_handle_arity_3_const { - template <typename T, - void (T::*)(request&, response&, typename MW::context&) const = - &T::before_handle> + template <typename T, void (T::*)(request&, response&, typename MW::context&) + const = &T::before_handle> struct get {}; }; template <typename MW> struct check_before_handle_arity_3 { - template <typename T, - void (T::*)(request&, response&, typename MW::context&) = - &T::before_handle> + template <typename T, void (T::*)(request&, response&, + typename MW::context&) = &T::before_handle> struct get {}; }; template <typename MW> struct check_after_handle_arity_3_const { - template <typename T, - void (T::*)(request&, response&, typename MW::context&) const = - &T::after_handle> + template <typename T, void (T::*)(request&, response&, typename MW::context&) + const = &T::after_handle> struct get {}; }; template <typename MW> struct check_after_handle_arity_3 { - template <typename T, - void (T::*)(request&, response&, typename MW::context&) = - &T::after_handle> + template <typename T, void (T::*)(request&, response&, + typename MW::context&) = &T::after_handle> struct get {}; }; @@ -366,7 +362,6 @@ class Connection { {503, "HTTP/1.1 503 Service Unavailable\r\n"}, }; - buffers_.clear(); buffers_.reserve(20); @@ -382,8 +377,6 @@ class Connection { if (res.code >= 400 && res.body.empty()) res.body = statusCodes[res.code].substr(9); - - const static std::string crlf = "\r\n"; content_length_ = std::to_string(res.body.size()); @@ -403,11 +396,10 @@ class Connection { res.add_header(keep_alive_tag, keep_alive_value); } -buffers_.emplace_back(res.headers.data(), res.headers.size()); + buffers_.emplace_back(res.headers.data(), res.headers.size()); buffers_.emplace_back(crlf.data(), crlf.size()); - res_body_copy_.swap(res.body); - buffers_.emplace_back(res_body_copy_.data(), res_body_copy_.size()); + buffers_.emplace_back(res.body.data(), res.body.size()); do_write(); @@ -430,8 +422,12 @@ buffers_.emplace_back(res.headers.data(), res.headers.size()); bool error_while_reading = true; if (!ec) { bool ret = parser_.feed(buffer_.data(), bytes_transferred); - if (ret && adaptor_.is_open()) { + if (parser_.upgrade) { error_while_reading = false; + } else { + if (ret && adaptor_.is_open()) { + error_while_reading = false; + } } } else { CROW_LOG_ERROR << "Error while reading: " << ec.message(); @@ -485,7 +481,6 @@ buffers_.emplace_back(res.headers.data(), res.headers.size()); is_writing = false; res.clear(); - res_body_copy_.clear(); if (!ec) { if (close_connection_) { adaptor_.close(); @@ -544,7 +539,6 @@ buffers_.emplace_back(res.headers.data(), res.headers.size()); std::string content_length_; std::string date_str_; - std::string res_body_copy_; // boost::asio::deadline_timer deadline_; detail::dumb_timer_queue::key timer_cancel_key_; diff --git a/crow/include/crow/http_request.h b/crow/include/crow/http_request.h index acb417c8ab..0477233c2a 100644 --- a/crow/include/crow/http_request.h +++ b/crow/include/crow/http_request.h @@ -48,6 +48,9 @@ struct request { const std::string& get_header_value(const std::string& key) const { return crow::get_header_value(headers, key); } + /* + // These APIs were here, and it's unclear what their intent was, when + // io_service is a public member. They are commented out for now template <typename CompletionHandler> void post(CompletionHandler handler) { @@ -58,5 +61,6 @@ struct request { void dispatch(CompletionHandler handler) { io_service->dispatch(handler); } +*/ }; } diff --git a/crow/include/crow/http_response.h b/crow/include/crow/http_response.h index 3ba28727ba..c81446fb3d 100644 --- a/crow/include/crow/http_response.h +++ b/crow/include/crow/http_response.h @@ -17,8 +17,7 @@ struct response { std::string body; json::wvalue json_value; - // `headers' stores HTTP headers. - //ci_map headers; + std::shared_ptr<std::string> body_ptr; std::string headers; @@ -77,6 +76,7 @@ struct response { code = 200; headers.clear(); completed_ = false; + body_ptr.reset(); } void write(const std::string& body_part) { body += body_part; } diff --git a/include/token_authorization_middleware.hpp b/include/token_authorization_middleware.hpp index 0f6e66786f..214fd93ab6 100644 --- a/include/token_authorization_middleware.hpp +++ b/include/token_authorization_middleware.hpp @@ -2,6 +2,7 @@ #include <crow/http_request.h> #include <crow/http_response.h> +#include <boost/container/flat_set.hpp> namespace crow { @@ -12,7 +13,7 @@ struct TokenAuthorizationMiddleware { // it opens the possibility of exposure by and endpoint. // instead we should only pass some kind of "user" struct struct context { - std::string auth_token; + //std::string auth_token; }; TokenAuthorizationMiddleware(); @@ -22,6 +23,6 @@ struct TokenAuthorizationMiddleware { void after_handle(request& req, response& res, context& ctx); private: - std::string auth_token2; + boost::container::flat_set<std::string> auth_token2; }; }
\ No newline at end of file diff --git a/scripts/build_web_assets.py b/scripts/build_web_assets.py index 46bf199c6e..ee04e229fc 100755 --- a/scripts/build_web_assets.py +++ b/scripts/build_web_assets.py @@ -24,27 +24,45 @@ CONTENT_TYPES = { CPP_MIDDLE_BUFFER = """ CROW_ROUTE(app, "{relative_path_sha1}") ([](const crow::request& req, crow::response& res) {{ {CACHE_FOREVER_HEADER} - res.add_header("ETag", "{sha1}"); - if (req.headers.count("If-None-Match") == 1) {{ - if (req.get_header_value("If-None-Match") == "{sha1}") {{ - res.code = 304; - res.end(); - return; - }} + std::string sha1("{sha1}"); + res.add_header(etag_string, sha1); + + if (req.get_header_value(if_none_match_string) == sha1) {{ + res.code = 304; + }} else {{ + res.code = 200; + // TODO, if you have a browser from the dark ages that doesn't support gzip, + // unzip it before sending based on Accept-Encoding header + res.add_header(content_encoding_string, {content_encoding}); + res.add_header(content_type_string, "{content_type}"); + + res.write(staticassets::{relative_path_escaped}); }} - - res.code = 200; - // TODO, if you have a browser from the dark ages that doesn't support gzip, - // unzip it before sending based on Accept-Encoding header - res.add_header("Content-Encoding", {content_encoding}); - res.add_header("Content-Type", "{content_type}"); - - res.write(staticassets::{relative_path_escaped}); - res.end(); }}); + """ +HPP_START_BUFFER = ("#pragma once\n" + "\n" + "#include <string>\n" + "\n" + "#include <crow/app.h>\n" + "#include <crow/http_request.h>\n" + "#include <crow/http_response.h>\n" + "\n" + "#include <crow/routing.h>\n" + "\n" + "namespace crow {\n" + "namespace webassets {\n" + "static const std::string gzip_string = \"gzip\";\n" + "static const std::string none_string = \"none\";\n" + "static const std::string if_none_match_string = \"If-None-Match\";\n" + "static const std::string content_encoding_string = \"Content-Encoding\";\n" + "static const std::string content_type_string = \"Content-Type\";\n" + "static const std::string etag_string = \"ETag\";\n" + ) + def twos_comp(val, bits): """compute the 2's compliment of int value val""" @@ -230,21 +248,7 @@ def main(): total_payload_size += len(file_content) with open(args.output.replace("cpp", "hpp"), 'w') as hpp_output: - hpp_output.write("#pragma once\n" - "\n" - "#include <string>\n" - "\n" - "#include <crow/app.h>\n" - "#include <crow/http_request.h>\n" - "#include <crow/http_response.h>\n" - "\n" - "#include <crow/routing.h>\n" - "\n" - "namespace crow {\n" - "namespace webassets {\n" - "static const std::string gzip_string = \"gzip\";\n" - "static const std::string none_string = \"none\";\n" - ) + hpp_output.write(HPP_START_BUFFER) hpp_output.write("struct staticassets {\n") for full_filepath in dependency_ordered_file_list: diff --git a/src/token_authorization_middleware.cpp b/src/token_authorization_middleware.cpp index 4bac9b4fb0..508bfd982c 100644 --- a/src/token_authorization_middleware.cpp +++ b/src/token_authorization_middleware.cpp @@ -2,6 +2,7 @@ #include <unordered_map> #include <boost/algorithm/string/predicate.hpp> +#include <security/pam_appl.h> #include <base64.hpp> #include <token_authorization_middleware.hpp> #include <crow/logging.h> @@ -12,10 +13,67 @@ using random_bytes_engine = std::independent_bits_engine<std::default_random_engine, CHAR_BIT, unsigned char>; -TokenAuthorizationMiddleware::TokenAuthorizationMiddleware() - : auth_token2(""){ +// function used to get user input +int pam_function_conversation(int num_msg, const struct pam_message** msg, + struct pam_response** resp, void* appdata_ptr) { + char* pass = (char*)malloc(strlen((char*)appdata_ptr) + 1); + strcpy(pass, (char*)appdata_ptr); - }; + int i; + + *resp = (pam_response*)calloc(num_msg, sizeof(struct pam_response)); + + for (i = 0; i < num_msg; ++i) { + /* Ignore all PAM messages except prompting for hidden input */ + if (msg[i]->msg_style != PAM_PROMPT_ECHO_OFF) continue; + + /* Assume PAM is only prompting for the password as hidden input */ + resp[i]->resp = pass; + } + + return PAM_SUCCESS; +} + +bool authenticate_user_pam(const std::string& username, + const std::string& password) { + const struct pam_conv local_conversation = {pam_function_conversation, + (char*)password.c_str()}; + pam_handle_t* local_auth_handle = NULL; // this gets set by pam_start + + int retval; + retval = pam_start("su", username.c_str(), &local_conversation, + &local_auth_handle); + + if (retval != PAM_SUCCESS) { + printf("pam_start returned: %d\n ", retval); + return false; + } + + retval = pam_authenticate(local_auth_handle, + PAM_SILENT | PAM_DISALLOW_NULL_AUTHTOK); + + if (retval != PAM_SUCCESS) { + if (retval == PAM_AUTH_ERR) { + printf("Authentication failure.\n"); + } else { + printf("pam_authenticate returned %d\n", retval); + } + return false; + } + + printf("Authenticated.\n"); + retval = pam_end(local_auth_handle, retval); + + if (retval != PAM_SUCCESS) { + printf("pam_end returned\n"); + return false; + } + + return true; +} + +TokenAuthorizationMiddleware::TokenAuthorizationMiddleware(){ +}; void TokenAuthorizationMiddleware::before_handle(crow::request& req, response& res, context& ctx) { @@ -38,9 +96,10 @@ void TokenAuthorizationMiddleware::before_handle(crow::request& req, // TODO this is total hackery to allow the login page to work before the // user is authenticated. Also, it will be quite slow for all pages instead // of a one time hit for the whitelist entries. Ideally, this should be - // done - // in the url router handler, with tagged routes for the whitelist entries. - // Another option would be to whitelist a minimal + // done in the url router handler, with tagged routes for the whitelist + // entries. Another option would be to whitelist a minimal for based page + // that didn't + // load the full angular UI until after login return; } @@ -63,7 +122,7 @@ void TokenAuthorizationMiddleware::before_handle(crow::request& req, auto password = login_credentials["password"].s(); // TODO(ed) pull real passwords from PAM - if (username == "dude" && password == "dude") { + if (authenticate_user_pam(username, password)) { crow::json::wvalue x; // TODO(ed) the RNG should be initialized at start, not every time we @@ -72,16 +131,14 @@ void TokenAuthorizationMiddleware::before_handle(crow::request& req, random_bytes_engine rbe; std::string token('a', 20); // TODO(ed) for some reason clang-tidy finds a divide by zero error in - // cstdlibc here - // commented out for now. Needs investigation + // cstdlibc here commented out for now. Needs investigation std::generate(begin(token), end(token), std::ref(rbe)); // NOLINT std::string encoded_token; base64::base64_encode(token, encoded_token); - ctx.auth_token = encoded_token; - this->auth_token2 = encoded_token; + // ctx.auth_token = encoded_token; + this->auth_token2.insert(encoded_token); - auto auth_token = ctx.auth_token; - x["token"] = auth_token; + x["token"] = encoded_token; res.write(json::dump(x)); res.add_header("Content-Type", "application/json"); @@ -92,10 +149,6 @@ void TokenAuthorizationMiddleware::before_handle(crow::request& req, } } - } else if (req.url == "/logout") { - this->auth_token2 = ""; - res.code = 200; - res.end(); } else { // Normal, non login, non static file request // Check to make sure we're logged in if (this->auth_token2.empty()) { @@ -114,16 +167,26 @@ void TokenAuthorizationMiddleware::before_handle(crow::request& req, return_unauthorized(); return; } - + std::string auth_key = auth_header.substr(6); // TODO(ed), use span here instead of constructing a new string - if (auth_header.substr(6) != this->auth_token2) { + if (this->auth_token2.find(auth_key) == this->auth_token2.end()) { return_unauthorized(); return; } + + if (req.url == "/logout") { + this->auth_token2.erase(auth_key); + res.code = 200; + res.end(); + return; + } + // else let the request continue unharmed } } void TokenAuthorizationMiddleware::after_handle(request& req, response& res, - context& ctx) {} + context& ctx) { + // Do nothing +} } diff --git a/src/webserver_main.cpp b/src/webserver_main.cpp index f47eb797a8..404e15cd12 100644 --- a/src/webserver_main.cpp +++ b/src/webserver_main.cpp @@ -228,11 +228,15 @@ int main(int argc, char** argv) { g3::initializeLogging(worker.get()); auto sink_handle = worker->addSink(std::make_unique<crow::ColorCoutSink>(), &crow::ColorCoutSink::ReceiveLogMessage); - + bool enable_ssl = true; std::string ssl_pem_file("server.pem"); - ensuressl::ensure_openssl_key_present_and_valid(ssl_pem_file); - crow::App<crow::TokenAuthorizationMiddleware, crow::SecurityHeadersMiddleware> + if (enable_ssl) { + ensuressl::ensure_openssl_key_present_and_valid(ssl_pem_file); + } + + crow::App< + crow::TokenAuthorizationMiddleware, crow::SecurityHeadersMiddleware> app; crow::webassets::request_routes(app); @@ -309,13 +313,33 @@ int main(int argc, char** argv) { return j; }); + + CROW_ROUTE(app, "/intel/firmwareupload") + .methods("POST"_method)([](const crow::request& req) { + // TODO(ed) handle errors here (file exists already and is locked, ect) + std::ofstream out( + "/tmp/fw_update_image", + std::ofstream::out | std::ofstream::binary | std::ofstream::trunc); + out << req.body; + out.close(); + + crow::json::wvalue j; + j["status"] = "Upload Successfull"; + + return j; + }); + LOG(DEBUG) << "Building SSL context"; - auto ssl_context = ensuressl::get_ssl_context(ssl_pem_file); + int port = 18080; LOG(DEBUG) << "Starting webserver on port " << port; - app.port(port) - .ssl(std::move(ssl_context)) - //.concurrency(4) - .run(); + app.port(port); + if (enable_ssl) { + LOG(DEBUG) << "SSL Enabled"; + auto ssl_context = ensuressl::get_ssl_context(ssl_pem_file); + app.ssl(std::move(ssl_context)); + } + app.concurrency(4); + app.run(); } diff --git a/static/CMakeLists.txt b/static/CMakeLists.txt index 183eeb2f43..29d5921b6b 100644 --- a/static/CMakeLists.txt +++ b/static/CMakeLists.txt @@ -1,22 +1,24 @@ set(JAVASCRIPT_ASSETS js/selController.js js/lodash.core.js - js/ui-bootstrap-tpls-2.1.3.js + js/ui-bootstrap-tpls-2.5.0.js js/angular-cookies.js js/angular-websocket.js js/angular-ui-router.js js/kvmController.js js/loginController.js js/ipmiController.js + js/fwupdateController.js + js/mainController.js js/versionController.js js/sensorController.js - js/angular-resource.js js/angular-sanitize.js js/bmcApp.js js/base64.js js/angular-animate.js js/run_prettify.js js/angular.js + js/angular-ui-router-uib-modal.js noVNC/core/inflator.js noVNC/core/input/xtscancodes.js @@ -55,10 +57,13 @@ set(HTML_ASSETS partial-kvm.html partial-sensor.html partial-systeminfo.html + partial-fwupdate.html + partial-fwupdateconfirm.html ) set(OTHER_ASSETS img/logo.png + img/blur-bg.jpg fonts/fontawesome-webfont.woff ) diff --git a/static/css/intel.css b/static/css/intel.css index 5ce5e38946..6915f8adac 100644 --- a/static/css/intel.css +++ b/static/css/intel.css @@ -1,202 +1,307 @@ /* navbar */ + .navbar-inverse { background-color: #0071c5; border-color: #E7E7E7; } + .navbar-inverse .navbar-brand { - color: #f3f3f3; + color: #f3f3f3; } + .navbar-inverse .navbar-brand:hover, .navbar-inverse .navbar-brand:focus { - color: #fff; - background-color: transparent; + color: #fff; + background-color: transparent; } + /* Make the brand icon look right */ + .navbar-brand { - padding: 0px 0px; + padding: 0px 0px; } .navbar-inverse .navbar-text { - color: #f3f3f3; + color: #f3f3f3; } -.navbar-inverse .navbar-nav > li > a { - color: #f3f3f3; + +.navbar-inverse .navbar-nav>li>a { + color: #f3f3f3; } -.navbar-inverse .navbar-nav > li > a:hover, -.navbar-inverse .navbar-nav > li > a:focus { - color: #fff; - background-color: transparent; + +.navbar-inverse .navbar-nav>li>a:hover, +.navbar-inverse .navbar-nav>li>a:focus { + color: #fff; + background-color: transparent; } -.navbar-inverse .navbar-nav > .active > a, -.navbar-inverse .navbar-nav > .active > a:hover, -.navbar-inverse .navbar-nav > .active > a:focus { - color: #fff; - background-color: #080808; + +.navbar-inverse .navbar-nav>.active>a, +.navbar-inverse .navbar-nav>.active>a:hover, +.navbar-inverse .navbar-nav>.active>a:focus { + color: #fff; + background-color: #080808; } -.navbar-inverse .navbar-nav > .disabled > a, -.navbar-inverse .navbar-nav > .disabled > a:hover, -.navbar-inverse .navbar-nav > .disabled > a:focus { - color: #444; - background-color: transparent; + +.navbar-inverse .navbar-nav>.disabled>a, +.navbar-inverse .navbar-nav>.disabled>a:hover, +.navbar-inverse .navbar-nav>.disabled>a:focus { + color: #444; + background-color: transparent; } + .navbar-inverse .navbar-toggle { - border-color: #333; + border-color: #333; } + .navbar-inverse .navbar-toggle:hover, .navbar-inverse .navbar-toggle:focus { - background-color: #333; + background-color: #333; } + .navbar-inverse .navbar-toggle .icon-bar { - background-color: #fff; + background-color: #fff; } + .navbar-inverse .navbar-collapse, .navbar-inverse .navbar-form { - border-color: #0071c5; -} -.navbar-inverse .navbar-nav > .open > a, -.navbar-inverse .navbar-nav > .open > a:hover, -.navbar-inverse .navbar-nav > .open > a:focus { - color: #fff; - background-color: #0071c5; -} -@media (max-width: 767px) { - .navbar-inverse .navbar-nav .open .dropdown-menu > .dropdown-header { border-color: #0071c5; - } - .navbar-inverse .navbar-nav .open .dropdown-menu .divider { - background-color: #0071c5; - } - .navbar-inverse .navbar-nav .open .dropdown-menu > li > a { - color: #f3f3f3; - } - .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:hover, - .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:focus { - color: #fff; - background-color: transparent; - } - .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a, - .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:hover, - .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:focus { +} + +.navbar-inverse .navbar-nav>.open>a, +.navbar-inverse .navbar-nav>.open>a:hover, +.navbar-inverse .navbar-nav>.open>a:focus { color: #fff; background-color: #0071c5; - } - .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a, - .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:hover, - .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:focus { - color: #444; - background-color: transparent; - } } + +@media (max-width: 767px) { + .navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header { + border-color: #0071c5; + } + .navbar-inverse .navbar-nav .open .dropdown-menu .divider { + background-color: #0071c5; + } + .navbar-inverse .navbar-nav .open .dropdown-menu>li>a { + color: #f3f3f3; + } + .navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover, + .navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus { + color: #fff; + background-color: transparent; + } + .navbar-inverse .navbar-nav .open .dropdown-menu>.active>a, + .navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover, + .navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus { + color: #fff; + background-color: #0071c5; + } + .navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a, + .navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover, + .navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus { + color: #444; + background-color: transparent; + } +} + .navbar-inverse .navbar-link { - color: #f3f3f3; + color: #f3f3f3; } + .navbar-inverse .navbar-link:hover { - color: #fff; + color: #fff; } + .navbar-inverse .btn-link { - color: #f3f3f3; + color: #f3f3f3; } + .navbar-inverse .btn-link:hover, .navbar-inverse .btn-link:focus { - color: #fff; + color: #fff; } + .navbar-inverse .btn-link[disabled]:hover, fieldset[disabled] .navbar-inverse .btn-link:hover, .navbar-inverse .btn-link[disabled]:focus, fieldset[disabled] .navbar-inverse .btn-link:focus { - color: #444; + color: #444; } + /* Collapse the navbar at 900 pixels*/ + @media (max-width: 900px) { - .navbar-header { - float: none; - } - .navbar-left,.navbar-right { - float: none !important; - } - .navbar-toggle { - display: block; - } - .navbar-collapse { - border-top: 1px solid transparent; - box-shadow: inset 0 1px 0 rgba(255,255,255,0.1); - } - .navbar-fixed-top { - top: 0; - border-width: 0 0 1px; - } - .navbar-collapse.collapse { - display: none!important; - } - .navbar-nav { - float: none!important; - margin-top: 7.5px; - } - .navbar-nav>li { - float: none; - } - .navbar-nav>li>a { - padding-top: 10px; - padding-bottom: 10px; - } - .collapse.in{ - display:block !important; - } -} - - -.system-status-table > tbody > tr > td, -.system-status-table > thead > tr > th{ - text-align: left; + .navbar-header { + float: none; + } + .navbar-left, + .navbar-right { + float: none !important; + } + .navbar-toggle { + display: block; + } + .navbar-collapse { + border-top: 1px solid transparent; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1); + } + .navbar-fixed-top { + top: 0; + border-width: 0 0 1px; + } + .navbar-collapse.collapse { + display: none!important; + } + .navbar-nav { + float: none!important; + margin-top: 7.5px; + } + .navbar-nav>li { + float: none; + } + .navbar-nav>li>a { + padding-top: 10px; + padding-bottom: 10px; + } + .collapse.in { + display: block !important; + } } -/*Push the first column right*/ -.system-status-table{ - font-size: small; +.system-status-table>tbody>tr>td, +.system-status-table>thead>tr>th { + text-align: left; } -.col-fixed-450{ - width:450px; - position:fixed; - height:100%; +/*Push the first column right*/ + +.system-status-table { + font-size: small; } +.col-fixed-450 { + width: 450px; + position: fixed; + height: 100%; +} /* JSON pretty print for the Rest API*/ + + /* source https://rawgit.com/google/code-prettify/master/styles/index.html*/ + + /* Pretty printing styles. Used with prettify.js. */ + + /* Vim sunburst theme by David Leibovic */ -pre .str, code .str { color: #65B042; } /* string - green */ -pre .kwd, code .kwd { color: #E28964; } /* keyword - dark pink */ -pre .com, code .com { color: #AEAEAE; font-style: italic; } /* comment - gray */ -pre .typ, code .typ { color: #89bdff; } /* type - light blue */ -pre .lit, code .lit { color: #3387CC; } /* literal - blue */ -pre .pun, code .pun { color: #fff; } /* punctuation - white */ -pre .pln, code .pln { color: #fff; } /* plaintext - white */ -pre .tag, code .tag { color: #89bdff; } /* html/xml tag - light blue */ -pre .atn, code .atn { color: #bdb76b; } /* html/xml attribute name - khaki */ -pre .atv, code .atv { color: #65B042; } /* html/xml attribute value - green */ -pre .dec, code .dec { color: #3387CC; } /* decimal - blue */ +pre .str, +code .str { + color: #65B042; +} -pre.prettyprint, code.prettyprint { - background-color: #000; - border-radius: 8px; + +/* string - green */ + +pre .kwd, +code .kwd { + color: #E28964; } -pre.prettyprint { - width: 95%; - margin: 1em auto; - padding: 1em; - white-space: pre-wrap; + +/* keyword - dark pink */ + +pre .com, +code .com { + color: #AEAEAE; + font-style: italic; +} + + +/* comment - gray */ + +pre .typ, +code .typ { + color: #89bdff; +} + + +/* type - light blue */ + +pre .lit, +code .lit { + color: #3387CC; +} + + +/* literal - blue */ + +pre .pun, +code .pun { + color: #fff; +} + + +/* punctuation - white */ + +pre .pln, +code .pln { + color: #fff; +} + + +/* plaintext - white */ + +pre .tag, +code .tag { + color: #89bdff; +} + + +/* html/xml tag - light blue */ + +pre .atn, +code .atn { + color: #bdb76b; +} + + +/* html/xml attribute name - khaki */ + +pre .atv, +code .atv { + color: #65B042; +} + + +/* html/xml attribute value - green */ + +pre .dec, +code .dec { + color: #3387CC; } -.table td.fit, +/* decimal - blue */ + +pre.prettyprint, +code.prettyprint { + background-color: #000; + border-radius: 8px; +} + +pre.prettyprint { + width: 95%; + margin: 1em auto; + padding: 1em; + white-space: pre-wrap; +} + +.table td.fit, .table th.fit { white-space: nowrap; width: 1%; @@ -204,367 +309,2576 @@ pre.prettyprint { /* Specify class=linenums on a pre to get line numbering */ -ol.linenums { margin-top: 0; margin-bottom: 0; color: #AEAEAE; } /* IE indents via margin-left */ -li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8 { list-style-type: none } + +ol.linenums { + margin-top: 0; + margin-bottom: 0; + color: #AEAEAE; +} + + +/* IE indents via margin-left */ + +li.L0, +li.L1, +li.L2, +li.L3, +li.L5, +li.L6, +li.L7, +li.L8 { + list-style-type: none +} + + /* Alternate shading for lines */ -li.L1,li.L3,li.L5,li.L7,li.L9 { } + +li.L1, +li.L3, +li.L5, +li.L7, +li.L9 {} @media print { - pre .str, code .str { color: #060; } - pre .kwd, code .kwd { color: #006; font-weight: bold; } - pre .com, code .com { color: #600; font-style: italic; } - pre .typ, code .typ { color: #404; font-weight: bold; } - pre .lit, code .lit { color: #044; } - pre .pun, code .pun { color: #440; } - pre .pln, code .pln { color: #000; } - pre .tag, code .tag { color: #006; font-weight: bold; } - pre .atn, code .atn { color: #404; } - pre .atv, code .atv { color: #060; } + pre .str, + code .str { + color: #060; + } + pre .kwd, + code .kwd { + color: #006; + font-weight: bold; + } + pre .com, + code .com { + color: #600; + font-style: italic; + } + pre .typ, + code .typ { + color: #404; + font-weight: bold; + } + pre .lit, + code .lit { + color: #044; + } + pre .pun, + code .pun { + color: #440; + } + pre .pln, + code .pln { + color: #000; + } + pre .tag, + code .tag { + color: #006; + font-weight: bold; + } + pre .atn, + code .atn { + color: #404; + } + pre .atv, + code .atv { + color: #060; + } } + + /* * Component: Box * -------------- */ + .box { - position: relative; - border-radius: 3px; - background: #ffffff; - border-top: 3px solid #d2d6de; - margin-bottom: 20px; - width: 100%; - box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); + position: relative; + border-radius: 3px; + background: #ffffff; + border-top: 3px solid #d2d6de; + margin-bottom: 20px; + width: 100%; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); } + .box.box-primary { - border-top-color: #0071c5; + border-top-color: #0071c5; } + .box.box-info { - border-top-color: #00c0ef; + border-top-color: #00c0ef; } + .box.box-danger { - border-top-color: #dd4b39; + border-top-color: #dd4b39; } + .box.box-warning { - border-top-color: #f39c12; + border-top-color: #f39c12; } + .box.box-success { - border-top-color: #00a65a; + border-top-color: #00a65a; } + .box.box-default { - border-top-color: #d2d6de; + border-top-color: #d2d6de; } + .box.collapsed-box .box-body, .box.collapsed-box .box-footer { - display: none; + display: none; } -.box .nav-stacked > li { - border-bottom: 1px solid #f4f4f4; - margin: 0; + +.box .nav-stacked>li { + border-bottom: 1px solid #f4f4f4; + margin: 0; } -.box .nav-stacked > li:last-of-type { - border-bottom: none; + +.box .nav-stacked>li:last-of-type { + border-bottom: none; } + .box.height-control .box-body { - max-height: 300px; - overflow: auto; + max-height: 300px; + overflow: auto; } + .box .border-right { - border-right: 1px solid #f4f4f4; + border-right: 1px solid #f4f4f4; } + .box .border-left { - border-left: 1px solid #f4f4f4; + border-left: 1px solid #f4f4f4; } + .box.box-solid { - border-top: 0; + border-top: 0; } -.box.box-solid > .box-header .btn.btn-default { - background: transparent; + +.box.box-solid>.box-header .btn.btn-default { + background: transparent; } -.box.box-solid > .box-header .btn:hover, -.box.box-solid > .box-header a:hover { - background: rgba(0, 0, 0, 0.1); + +.box.box-solid>.box-header .btn:hover, +.box.box-solid>.box-header a:hover { + background: rgba(0, 0, 0, 0.1); } + .box.box-solid.box-default { - border: 1px solid #d2d6de; + border: 1px solid #d2d6de; } -.box.box-solid.box-default > .box-header { - color: #444444; - background: #d2d6de; - background-color: #d2d6de; + +.box.box-solid.box-default>.box-header { + color: #444444; + background: #d2d6de; + background-color: #d2d6de; } -.box.box-solid.box-default > .box-header a, -.box.box-solid.box-default > .box-header .btn { - color: #444444; + +.box.box-solid.box-default>.box-header a, +.box.box-solid.box-default>.box-header .btn { + color: #444444; } + .box.box-solid.box-primary { - border: 1px solid #0071c5; + border: 1px solid #0071c5; } -.box.box-solid.box-primary > .box-header { - color: #ffffff; - background: #0071c5; - background-color: #0071c5; + +.box.box-solid.box-primary>.box-header { + color: #ffffff; + background: #0071c5; + background-color: #0071c5; } -.box.box-solid.box-primary > .box-header a, -.box.box-solid.box-primary > .box-header .btn { - color: #ffffff; + +.box.box-solid.box-primary>.box-header a, +.box.box-solid.box-primary>.box-header .btn { + color: #ffffff; } + .box.box-solid.box-info { - border: 1px solid #00c0ef; + border: 1px solid #00c0ef; } -.box.box-solid.box-info > .box-header { - color: #ffffff; - background: #00c0ef; - background-color: #00c0ef; + +.box.box-solid.box-info>.box-header { + color: #ffffff; + background: #00c0ef; + background-color: #00c0ef; } -.box.box-solid.box-info > .box-header a, -.box.box-solid.box-info > .box-header .btn { - color: #ffffff; + +.box.box-solid.box-info>.box-header a, +.box.box-solid.box-info>.box-header .btn { + color: #ffffff; } + .box.box-solid.box-danger { - border: 1px solid #dd4b39; + border: 1px solid #dd4b39; } -.box.box-solid.box-danger > .box-header { - color: #ffffff; - background: #dd4b39; - background-color: #dd4b39; + +.box.box-solid.box-danger>.box-header { + color: #ffffff; + background: #dd4b39; + background-color: #dd4b39; } -.box.box-solid.box-danger > .box-header a, -.box.box-solid.box-danger > .box-header .btn { - color: #ffffff; + +.box.box-solid.box-danger>.box-header a, +.box.box-solid.box-danger>.box-header .btn { + color: #ffffff; } + .box.box-solid.box-warning { - border: 1px solid #f39c12; + border: 1px solid #f39c12; } -.box.box-solid.box-warning > .box-header { - color: #ffffff; - background: #f39c12; - background-color: #f39c12; + +.box.box-solid.box-warning>.box-header { + color: #ffffff; + background: #f39c12; + background-color: #f39c12; } -.box.box-solid.box-warning > .box-header a, -.box.box-solid.box-warning > .box-header .btn { - color: #ffffff; + +.box.box-solid.box-warning>.box-header a, +.box.box-solid.box-warning>.box-header .btn { + color: #ffffff; } + .box.box-solid.box-success { - border: 1px solid #00a65a; + border: 1px solid #00a65a; } -.box.box-solid.box-success > .box-header { - color: #ffffff; - background: #00a65a; - background-color: #00a65a; + +.box.box-solid.box-success>.box-header { + color: #ffffff; + background: #00a65a; + background-color: #00a65a; } -.box.box-solid.box-success > .box-header a, -.box.box-solid.box-success > .box-header .btn { - color: #ffffff; + +.box.box-solid.box-success>.box-header a, +.box.box-solid.box-success>.box-header .btn { + color: #ffffff; } -.box.box-solid > .box-header > .box-tools .btn { - border: 0; - box-shadow: none; + +.box.box-solid>.box-header>.box-tools .btn { + border: 0; + box-shadow: none; } -.box.box-solid[class*='bg'] > .box-header { - color: #fff; + +.box.box-solid[class*='bg']>.box-header { + color: #fff; } -.box .box-group > .box { - margin-bottom: 5px; + +.box .box-group>.box { + margin-bottom: 5px; } + .box .knob-label { - text-align: center; - color: #333; - font-weight: 100; - font-size: 12px; - margin-bottom: 0.3em; -} -.box > .overlay, -.overlay-wrapper > .overlay, -.box > .loading-img, -.overlay-wrapper > .loading-img { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; + text-align: center; + color: #333; + font-weight: 100; + font-size: 12px; + margin-bottom: 0.3em; +} + +.box>.overlay, +.overlay-wrapper>.overlay, +.box>.loading-img, +.overlay-wrapper>.loading-img { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; } + .box .overlay, .overlay-wrapper .overlay { - z-index: 50; - background: rgba(255, 255, 255, 0.7); - border-radius: 3px; -} -.box .overlay > .fa, -.overlay-wrapper .overlay > .fa { - position: absolute; - top: 50%; - left: 50%; - margin-left: -15px; - margin-top: -15px; - color: #000; - font-size: 30px; + z-index: 50; + background: rgba(255, 255, 255, 0.7); + border-radius: 3px; +} + +.box .overlay>.fa, +.overlay-wrapper .overlay>.fa { + position: absolute; + top: 50%; + left: 50%; + margin-left: -15px; + margin-top: -15px; + color: #000; + font-size: 30px; } + .box .overlay.dark, .overlay-wrapper .overlay.dark { - background: rgba(0, 0, 0, 0.5); + background: rgba(0, 0, 0, 0.5); } + .box-header:before, .box-body:before, .box-footer:before, .box-header:after, .box-body:after, .box-footer:after { - content: " "; - display: table; + content: " "; + display: table; } + .box-header:after, .box-body:after, .box-footer:after { - clear: both; + clear: both; } + .box-header { - color: #444; - display: block; - padding: 10px; - position: relative; + color: #444; + display: block; + padding: 10px; + position: relative; } + .box-header.with-border { - border-bottom: 1px solid #f4f4f4; + border-bottom: 1px solid #f4f4f4; } + .collapsed-box .box-header.with-border { - border-bottom: none; + border-bottom: none; } -.box-header > .fa, -.box-header > .glyphicon, -.box-header > .ion, + +.box-header>.fa, +.box-header>.glyphicon, +.box-header>.ion, .box-header .box-title { - display: inline-block; - font-size: 18px; - margin: 0; - line-height: 1; + display: inline-block; + font-size: 18px; + margin: 0; + line-height: 1; } -.box-header > .fa, -.box-header > .glyphicon, -.box-header > .ion { - margin-right: 5px; + +.box-header>.fa, +.box-header>.glyphicon, +.box-header>.ion { + margin-right: 5px; } -.box-header > .box-tools { - position: absolute; - right: 10px; - top: 5px; + +.box-header>.box-tools { + position: absolute; + right: 10px; + top: 5px; } -.box-header > .box-tools [data-toggle="tooltip"] { - position: relative; + +.box-header>.box-tools [data-toggle="tooltip"] { + position: relative; } -.box-header > .box-tools.pull-right .dropdown-menu { - right: 0; - left: auto; + +.box-header>.box-tools.pull-right .dropdown-menu { + right: 0; + left: auto; } + .btn-box-tool { - padding: 5px; - font-size: 12px; - background: transparent; - color: #97a0b3; + padding: 5px; + font-size: 12px; + background: transparent; + color: #97a0b3; } + .open .btn-box-tool, .btn-box-tool:hover { - color: #606c84; + color: #606c84; } + .btn-box-tool.btn:active { - box-shadow: none; + box-shadow: none; } + .box-body { - border-top-left-radius: 0; - border-top-right-radius: 0; - border-bottom-right-radius: 3px; - border-bottom-left-radius: 3px; - padding: 10px; + border-top-left-radius: 0; + border-top-right-radius: 0; + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; + padding: 10px; } + .no-header .box-body { - border-top-right-radius: 3px; - border-top-left-radius: 3px; + border-top-right-radius: 3px; + border-top-left-radius: 3px; } -.box-body > .table { - margin-bottom: 0; + +.box-body>.table { + margin-bottom: 0; } + .box-body .fc { - margin-top: 5px; + margin-top: 5px; } + .box-body .full-width-chart { - margin: -19px; + margin: -19px; } + .box-body.no-padding .full-width-chart { - margin: -9px; + margin: -9px; } + .box-body .box-pane { - border-top-left-radius: 0; - border-top-right-radius: 0; - border-bottom-right-radius: 0; - border-bottom-left-radius: 3px; + border-top-left-radius: 0; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 3px; } + .box-body .box-pane-right { - border-top-left-radius: 0; - border-top-right-radius: 0; - border-bottom-right-radius: 3px; - border-bottom-left-radius: 0; + border-top-left-radius: 0; + border-top-right-radius: 0; + border-bottom-right-radius: 3px; + border-bottom-left-radius: 0; } + .box-footer { - border-top-left-radius: 0; - border-top-right-radius: 0; - border-bottom-right-radius: 3px; - border-bottom-left-radius: 3px; - border-top: 1px solid #f4f4f4; - padding: 10px; - background-color: #ffffff; + border-top-left-radius: 0; + border-top-right-radius: 0; + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; + border-top: 1px solid #f4f4f4; + padding: 10px; + background-color: #ffffff; } + .chart-legend { - margin: 10px 0; + margin: 10px 0; } + @media (max-width: 991px) { - .chart-legend > li { - float: left; - margin-right: 10px; - } + .chart-legend>li { + float: left; + margin-right: 10px; + } } + .box-comments { - background: #f7f7f7; + background: #f7f7f7; } + .box-comments .box-comment { - padding: 8px 0; - border-bottom: 1px solid #eee; + padding: 8px 0; + border-bottom: 1px solid #eee; } + .box-comments .box-comment:before, .box-comments .box-comment:after { - content: " "; - display: table; + content: " "; + display: table; } + .box-comments .box-comment:after { - clear: both; + clear: both; } + .box-comments .box-comment:last-of-type { - border-bottom: 0; + border-bottom: 0; } + .box-comments .box-comment:first-of-type { - padding-top: 0; + padding-top: 0; } + .box-comments .box-comment img { - float: left; + float: left; } + .box-comments .comment-text { - margin-left: 40px; - color: #555; + margin-left: 40px; + color: #555; } + .box-comments .username { - color: #444; - display: block; - font-weight: 600; + color: #444; + display: block; + font-weight: 600; } + .box-comments .text-muted { - font-weight: 400; - font-size: 12px; + font-weight: 400; + font-size: 12px; } -.table-striped-invert > tbody > tr:nth-child(2n+1) > td, .table-striped > tbody > tr:nth-child(2n+1) > th { - background-color: #ffffff; +.table-striped-invert>tbody>tr:nth-child(2n+1)>td, +.table-striped>tbody>tr:nth-child(2n+1)>th { + background-color: #ffffff; } - .div-fake-hidden { - width:0px; - height:0px; - overflow:hidden; -}
\ No newline at end of file + width: 0px; + height: 0px; + overflow: hidden; +} + +::-webkit-scrollbar { + width: .5em; + height: .5em +} + +body.blur-theme::before, +body.mobile.blur-theme .body-bg::before { + content: ''; + height: 100%; + top: 0; + left: 0; + z-index: -1; + width: 100%; + will-change: transform +} + +::-webkit-scrollbar-thumb { + background: #d9d9d9; + cursor: pointer +} + +::-webkit-scrollbar-track { + background: 0 0 +} + +body { + scrollbar-face-color: #d9d9d9; + scrollbar-track-color: transparent; + font: 14px/16px Roboto, sans-serif; + color: #666; + background-color: #F0F3F4 +} + +body, +html { + min-height: 100%; + min-width: 320px +} + +body.blur-theme::before { + position: fixed; + background: url(../../static/img/blur-bg.jpg) center center no-repeat; + background-size: cover +} + +body.mobile { + background: 0 0 +} + +body.mobile .body-bg { + display: block; + position: fixed; + top: 0; + left: 0; + bottom: 0; + right: 0; + background-attachment: inherit; + background-color: #F0F3F4 +} + +body.mobile.blur-theme .body-bg::before { + position: fixed; + background: url(../../static/img/blur-bg.jpg) center center no-repeat; + background-size: cover +} + +@media screen and (-ms-high-contrast:active), +(-ms-high-contrast:none) { + body, + html { + height: 100% + } + html { + overflow: hidden + } + body { + overflow: auto + } +} + +a { + -webkit-transition: color .5s ease; + transition: color .5s ease; + outline: 0!important +} + +.body-bg { + display: none +} + +.al-header { + display: block; + height: 49px; + margin: 0; + background-repeat: repeat-x; + position: relative; + z-index: 905; + color: #444 +} + +.al-main { + margin-left: 180px; + padding: 66px 0 34px; + min-height: 500px +} + +.al-footer { + height: 34px; + padding: 0 18px 0 180px; + width: 100%; + position: absolute; + display: block; + bottom: 0; + font-size: 13px; + color: #666; + -webkit-transition: padding-left .5s ease; + transition: padding-left .5s ease +} + +.al-footer-main { + float: left; + margin-left: 15px +} + +.al-copy { + float: left +} + +.al-footer-right { + float: right; + margin-right: 12px +} + +.al-share, +.al-share li { + list-style: none; + float: left +} + +.al-footer-right i { + margin: 0 4px; + color: #e85656; + font-size: 12px +} + +.al-footer-right a { + margin-left: 4px; + color: #666 +} + +.al-footer-right a:hover { + color: #e85656 +} + +.al-share { + margin: -6px 0 0 12px; + padding: 0 +} + +.al-share li { + margin-left: 16px +} + +.al-share li i { + cursor: pointer; + -webkit-transition: all .1s ease; + transition: all .1s ease; + color: #fff; + padding: 6px; + box-sizing: content-box; + font-size: 16px +} + +.al-share li i:hover { + -webkit-transform: scale(1.2); + transform: scale(1.2) +} + +.al-share li i.fa-facebook-square { + color: #3b5998 +} + +.al-share li i.fa-twitter-square { + color: #55acee +} + +.al-share li i.fa-google-plus-square { + color: #dd4b39 +} + +.al-content { + padding: 8px 32px 8px 40px +} + +@media screen and (max-width: 500px) { + .al-content { + padding: 8px 20px + } +} + +.vis-hidden { + visibility: hidden; + position: absolute; + top: -9999px; + left: -9999px +} + +.icon-down, +.icon-up { + width: 5px; + height: 13px; + display: block +} + +.icon-up { + background: url(../assets/img/arrow-green-up.svg) no-repeat +} + +.icon-down { + background: url(../assets/img/arrow-red-down.svg) no-repeat +} + +.disable-text-selection { + -webkit-touch-callout: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none +} + +.align-right { + text-align: right +} + +.amcharts-chart-div>a { + font-size: 6px!important +} + +.content-panel { + padding-left: 22px; + padding-top: 26px +} + +@media (max-width: 590px) { + .al-footer-right { + float: none; + margin-bottom: 19px; + margin-right: 0 + } + .al-footer { + height: 76px; + text-align: center + } + .al-main { + padding-bottom: 76px + } + .al-footer-main { + float: none; + display: inline-block + } +} + +.full-invisible, +.full-invisible * { + visibility: hidden!important +} + +.irs-grid-text { + color: #666 +} + +.btn.active.focus, +.btn.active:focus, +.btn.focus, +.btn:active.focus, +.btn:active:focus, +.btn:focus { + outline: 0 +} + +.btn { + border-radius: 5px; + -webkit-transition: all .1s ease; + transition: all .1s ease +} + +button.progress-button.btn.btn-danger, +button.progress-button.btn.btn-default, +button.progress-button.btn.btn-info, +button.progress-button.btn.btn-primary, +button.progress-button.btn.btn-success, +button.progress-button.btn.btn-warning { + border-radius: 0 +} + +.btn:hover { + -webkit-transform: scale(1.2); + transform: scale(1.2) +} + +.open>.btn.dropdown-toggle.btn.btn-primary { + background: #1b867b; + border-color: #1b867b +} + +.open>.btn.dropdown-toggle.btn-success { + background: #7a9d00; + border-color: #7a9d00 +} + +.open>.btn.dropdown-toggle.btn-info { + background: #2692b2; + border-color: #2692b2 +} + +.open>.btn.dropdown-toggle.btn-warning { + background: #be9c18; + border-color: #be9c18 +} + +.open>.btn.dropdown-toggle.btn-danger { + background: #c54949; + border-color: #c54949 +} + +button.btn.btn-primary { + background: #209e91; + border-color: #209e91 +} + +button.btn.btn-primary.disabled, +button.btn.btn-primary.disabled.active, +button.btn.btn-primary.disabled.focus, +button.btn.btn-primary.disabled:active, +button.btn.btn-primary.disabled:focus, +button.btn.btn-primary.disabled:hover, +button.btn.btn-primary[disabled], +button.btn.btn-primary[disabled].active, +button.btn.btn-primary[disabled].focus, +button.btn.btn-primary[disabled]:active, +button.btn.btn-primary[disabled]:focus, +button.btn.btn-primary[disabled]:hover, +fieldset[disabled] button.btn.btn-primary, +fieldset[disabled] button.btn.btn-primary.active, +fieldset[disabled] button.btn.btn-primary.focus, +fieldset[disabled] button.btn.btn-primary:active, +fieldset[disabled] button.btn.btn-primary:focus, +fieldset[disabled] button.btn.btn-primary:hover { + background: #209e91; + border-color: #2caa9d +} + +button.btn.btn-primary.disabled.active:hover, +button.btn.btn-primary.disabled.focus:hover, +button.btn.btn-primary.disabled:active:hover, +button.btn.btn-primary.disabled:focus:hover, +button.btn.btn-primary.disabled:hover, +button.btn.btn-primary.disabled:hover:hover, +button.btn.btn-primary[disabled].active:hover, +button.btn.btn-primary[disabled].focus:hover, +button.btn.btn-primary[disabled]:active:hover, +button.btn.btn-primary[disabled]:focus:hover, +button.btn.btn-primary[disabled]:hover, +button.btn.btn-primary[disabled]:hover:hover, +fieldset[disabled] button.btn.btn-primary.active:hover, +fieldset[disabled] button.btn.btn-primary.focus:hover, +fieldset[disabled] button.btn.btn-primary:active:hover, +fieldset[disabled] button.btn.btn-primary:focus:hover, +fieldset[disabled] button.btn.btn-primary:hover, +fieldset[disabled] button.btn.btn-primary:hover:hover { + -webkit-transform: none; + transform: none +} + +button.btn.btn-primary.active, +button.btn.btn-primary.focus, +button.btn.btn-primary:active, +button.btn.btn-primary:focus, +button.btn.btn-primary:hover { + background: #209e91; + border-color: #088679 +} + +button.btn.btn-primary:active, +button.btn.btn-primary:target { + background-color: #1b867b +} + +button.btn.btn-default { + border-width: 1px; + color: #666; + background: 0 0; + border-color: #d6d6d6 +} + +button.btn.btn-default.disabled, +button.btn.btn-default.disabled.active, +button.btn.btn-default.disabled.focus, +button.btn.btn-default.disabled:active, +button.btn.btn-default.disabled:focus, +button.btn.btn-default.disabled:hover, +button.btn.btn-default[disabled], +button.btn.btn-default[disabled].active, +button.btn.btn-default[disabled].focus, +button.btn.btn-default[disabled]:active, +button.btn.btn-default[disabled]:focus, +button.btn.btn-default[disabled]:hover, +fieldset[disabled] button.btn.btn-default, +fieldset[disabled] button.btn.btn-default.active, +fieldset[disabled] button.btn.btn-default.focus, +fieldset[disabled] button.btn.btn-default:active, +fieldset[disabled] button.btn.btn-default:focus, +fieldset[disabled] button.btn.btn-default:hover { + background: 0 0; + border-color: #e2e2e2 +} + +button.btn.btn-default.disabled.active:hover, +button.btn.btn-default.disabled.focus:hover, +button.btn.btn-default.disabled:active:hover, +button.btn.btn-default.disabled:focus:hover, +button.btn.btn-default.disabled:hover, +button.btn.btn-default.disabled:hover:hover, +button.btn.btn-default[disabled].active:hover, +button.btn.btn-default[disabled].focus:hover, +button.btn.btn-default[disabled]:active:hover, +button.btn.btn-default[disabled]:focus:hover, +button.btn.btn-default[disabled]:hover, +button.btn.btn-default[disabled]:hover:hover, +fieldset[disabled] button.btn.btn-default.active:hover, +fieldset[disabled] button.btn.btn-default.focus:hover, +fieldset[disabled] button.btn.btn-default:active:hover, +fieldset[disabled] button.btn.btn-default:focus:hover, +fieldset[disabled] button.btn.btn-default:hover, +fieldset[disabled] button.btn.btn-default:hover:hover { + -webkit-transform: none; + transform: none +} + +button.btn.btn-default.active, +button.btn.btn-default.focus, +button.btn.btn-default:active, +button.btn.btn-default:focus, +button.btn.btn-default:hover { + background: 0 0; + border-color: #bebebe +} + +button.btn.btn-default:active, +button.btn.btn-default:target { + background-color: rgba(0, 0, 0, .2); + color: #666 +} + +button.btn.btn-success { + background: #90b900; + border-color: #90b900 +} + +button.btn.btn-success.disabled, +button.btn.btn-success.disabled.active, +button.btn.btn-success.disabled.focus, +button.btn.btn-success.disabled:active, +button.btn.btn-success.disabled:focus, +button.btn.btn-success.disabled:hover, +button.btn.btn-success[disabled], +button.btn.btn-success[disabled].active, +button.btn.btn-success[disabled].focus, +button.btn.btn-success[disabled]:active, +button.btn.btn-success[disabled]:focus, +button.btn.btn-success[disabled]:hover, +fieldset[disabled] button.btn.btn-success, +fieldset[disabled] button.btn.btn-success.active, +fieldset[disabled] button.btn.btn-success.focus, +fieldset[disabled] button.btn.btn-success:active, +fieldset[disabled] button.btn.btn-success:focus, +fieldset[disabled] button.btn.btn-success:hover { + background: #90b900; + border-color: #9cc50c +} + +button.btn.btn-success.disabled.active:hover, +button.btn.btn-success.disabled.focus:hover, +button.btn.btn-success.disabled:active:hover, +button.btn.btn-success.disabled:focus:hover, +button.btn.btn-success.disabled:hover, +button.btn.btn-success.disabled:hover:hover, +button.btn.btn-success[disabled].active:hover, +button.btn.btn-success[disabled].focus:hover, +button.btn.btn-success[disabled]:active:hover, +button.btn.btn-success[disabled]:focus:hover, +button.btn.btn-success[disabled]:hover, +button.btn.btn-success[disabled]:hover:hover, +fieldset[disabled] button.btn.btn-success.active:hover, +fieldset[disabled] button.btn.btn-success.focus:hover, +fieldset[disabled] button.btn.btn-success:active:hover, +fieldset[disabled] button.btn.btn-success:focus:hover, +fieldset[disabled] button.btn.btn-success:hover, +fieldset[disabled] button.btn.btn-success:hover:hover { + -webkit-transform: none; + transform: none +} + +button.btn.btn-success.active, +button.btn.btn-success.focus, +button.btn.btn-success:active, +button.btn.btn-success:focus, +button.btn.btn-success:hover { + background: #90b900; + border-color: #78a100 +} + +button.btn.btn-success:active, +button.btn.btn-success:target { + background-color: #7a9d00 +} + +button.btn.btn-info { + background: #2dacd1; + border-color: #2dacd1 +} + +button.btn.btn-info.disabled, +button.btn.btn-info.disabled.active, +button.btn.btn-info.disabled.focus, +button.btn.btn-info.disabled:active, +button.btn.btn-info.disabled:focus, +button.btn.btn-info.disabled:hover, +button.btn.btn-info[disabled], +button.btn.btn-info[disabled].active, +button.btn.btn-info[disabled].focus, +button.btn.btn-info[disabled]:active, +button.btn.btn-info[disabled]:focus, +button.btn.btn-info[disabled]:hover, +fieldset[disabled] button.btn.btn-info, +fieldset[disabled] button.btn.btn-info.active, +fieldset[disabled] button.btn.btn-info.focus, +fieldset[disabled] button.btn.btn-info:active, +fieldset[disabled] button.btn.btn-info:focus, +fieldset[disabled] button.btn.btn-info:hover { + background: #2dacd1; + border-color: #39b8dd +} + +button.btn.btn-info.disabled.active:hover, +button.btn.btn-info.disabled.focus:hover, +button.btn.btn-info.disabled:active:hover, +button.btn.btn-info.disabled:focus:hover, +button.btn.btn-info.disabled:hover, +button.btn.btn-info.disabled:hover:hover, +button.btn.btn-info[disabled].active:hover, +button.btn.btn-info[disabled].focus:hover, +button.btn.btn-info[disabled]:active:hover, +button.btn.btn-info[disabled]:focus:hover, +button.btn.btn-info[disabled]:hover, +button.btn.btn-info[disabled]:hover:hover, +fieldset[disabled] button.btn.btn-info.active:hover, +fieldset[disabled] button.btn.btn-info.focus:hover, +fieldset[disabled] button.btn.btn-info:active:hover, +fieldset[disabled] button.btn.btn-info:focus:hover, +fieldset[disabled] button.btn.btn-info:hover, +fieldset[disabled] button.btn.btn-info:hover:hover { + -webkit-transform: none; + transform: none +} + +button.btn.btn-info.active, +button.btn.btn-info.focus, +button.btn.btn-info:active, +button.btn.btn-info:focus, +button.btn.btn-info:hover { + background: #2dacd1; + border-color: #1594b9 +} + +button.btn.btn-info:active, +button.btn.btn-info:target { + background-color: #2692b2 +} + +button.btn.btn-warning { + background: #dfb81c; + border-color: #dfb81c +} + +button.btn.btn-warning.disabled, +button.btn.btn-warning.disabled.active, +button.btn.btn-warning.disabled.focus, +button.btn.btn-warning.disabled:active, +button.btn.btn-warning.disabled:focus, +button.btn.btn-warning.disabled:hover, +button.btn.btn-warning[disabled], +button.btn.btn-warning[disabled].active, +button.btn.btn-warning[disabled].focus, +button.btn.btn-warning[disabled]:active, +button.btn.btn-warning[disabled]:focus, +button.btn.btn-warning[disabled]:hover, +fieldset[disabled] button.btn.btn-warning, +fieldset[disabled] button.btn.btn-warning.active, +fieldset[disabled] button.btn.btn-warning.focus, +fieldset[disabled] button.btn.btn-warning:active, +fieldset[disabled] button.btn.btn-warning:focus, +fieldset[disabled] button.btn.btn-warning:hover { + background: #dfb81c; + border-color: #ebc428 +} + +button.btn.btn-warning.disabled.active:hover, +button.btn.btn-warning.disabled.focus:hover, +button.btn.btn-warning.disabled:active:hover, +button.btn.btn-warning.disabled:focus:hover, +button.btn.btn-warning.disabled:hover, +button.btn.btn-warning.disabled:hover:hover, +button.btn.btn-warning[disabled].active:hover, +button.btn.btn-warning[disabled].focus:hover, +button.btn.btn-warning[disabled]:active:hover, +button.btn.btn-warning[disabled]:focus:hover, +button.btn.btn-warning[disabled]:hover, +button.btn.btn-warning[disabled]:hover:hover, +fieldset[disabled] button.btn.btn-warning.active:hover, +fieldset[disabled] button.btn.btn-warning.focus:hover, +fieldset[disabled] button.btn.btn-warning:active:hover, +fieldset[disabled] button.btn.btn-warning:focus:hover, +fieldset[disabled] button.btn.btn-warning:hover, +fieldset[disabled] button.btn.btn-warning:hover:hover { + -webkit-transform: none; + transform: none +} + +button.btn.btn-warning.active, +button.btn.btn-warning.focus, +button.btn.btn-warning:active, +button.btn.btn-warning:focus, +button.btn.btn-warning:hover { + background: #dfb81c; + border-color: #c7a004 +} + +button.btn.btn-warning:active, +button.btn.btn-warning:target { + background-color: #be9c18 +} + +button.btn.btn-danger { + background: #e85656; + border-color: #e85656 +} + +button.btn.btn-danger.disabled, +button.btn.btn-danger.disabled.active, +button.btn.btn-danger.disabled.focus, +button.btn.btn-danger.disabled:active, +button.btn.btn-danger.disabled:focus, +button.btn.btn-danger.disabled:hover, +button.btn.btn-danger[disabled], +button.btn.btn-danger[disabled].active, +button.btn.btn-danger[disabled].focus, +button.btn.btn-danger[disabled]:active, +button.btn.btn-danger[disabled]:focus, +button.btn.btn-danger[disabled]:hover, +fieldset[disabled] button.btn.btn-danger, +fieldset[disabled] button.btn.btn-danger.active, +fieldset[disabled] button.btn.btn-danger.focus, +fieldset[disabled] button.btn.btn-danger:active, +fieldset[disabled] button.btn.btn-danger:focus, +fieldset[disabled] button.btn.btn-danger:hover { + background: #e85656; + border-color: #f46262 +} + +button.btn.btn-danger.disabled.active:hover, +button.btn.btn-danger.disabled.focus:hover, +button.btn.btn-danger.disabled:active:hover, +button.btn.btn-danger.disabled:focus:hover, +button.btn.btn-danger.disabled:hover, +button.btn.btn-danger.disabled:hover:hover, +button.btn.btn-danger[disabled].active:hover, +button.btn.btn-danger[disabled].focus:hover, +button.btn.btn-danger[disabled]:active:hover, +button.btn.btn-danger[disabled]:focus:hover, +button.btn.btn-danger[disabled]:hover, +button.btn.btn-danger[disabled]:hover:hover, +fieldset[disabled] button.btn.btn-danger.active:hover, +fieldset[disabled] button.btn.btn-danger.focus:hover, +fieldset[disabled] button.btn.btn-danger:active:hover, +fieldset[disabled] button.btn.btn-danger:focus:hover, +fieldset[disabled] button.btn.btn-danger:hover, +fieldset[disabled] button.btn.btn-danger:hover:hover { + -webkit-transform: none; + transform: none +} + +button.btn.btn-danger.active, +button.btn.btn-danger.focus, +button.btn.btn-danger:active, +button.btn.btn-danger:focus, +button.btn.btn-danger:hover { + background: #e85656; + border-color: #d03e3e +} + +button.btn.btn-danger:active, +button.btn.btn-danger:target { + background-color: #c54949 +} + +button.btn.btn-inverse { + background: #949494; + border-color: #949494; + color: #fff +} + +button.btn.btn-inverse.disabled, +button.btn.btn-inverse.disabled.active, +button.btn.btn-inverse.disabled.focus, +button.btn.btn-inverse.disabled:active, +button.btn.btn-inverse.disabled:focus, +button.btn.btn-inverse.disabled:hover, +button.btn.btn-inverse[disabled], +button.btn.btn-inverse[disabled].active, +button.btn.btn-inverse[disabled].focus, +button.btn.btn-inverse[disabled]:active, +button.btn.btn-inverse[disabled]:focus, +button.btn.btn-inverse[disabled]:hover, +fieldset[disabled] button.btn.btn-inverse, +fieldset[disabled] button.btn.btn-inverse.active, +fieldset[disabled] button.btn.btn-inverse.focus, +fieldset[disabled] button.btn.btn-inverse:active, +fieldset[disabled] button.btn.btn-inverse:focus, +fieldset[disabled] button.btn.btn-inverse:hover { + background: #949494; + border-color: #a0a0a0 +} + +button.btn.btn-inverse.disabled.active:hover, +button.btn.btn-inverse.disabled.focus:hover, +button.btn.btn-inverse.disabled:active:hover, +button.btn.btn-inverse.disabled:focus:hover, +button.btn.btn-inverse.disabled:hover, +button.btn.btn-inverse.disabled:hover:hover, +button.btn.btn-inverse[disabled].active:hover, +button.btn.btn-inverse[disabled].focus:hover, +button.btn.btn-inverse[disabled]:active:hover, +button.btn.btn-inverse[disabled]:focus:hover, +button.btn.btn-inverse[disabled]:hover, +button.btn.btn-inverse[disabled]:hover:hover, +fieldset[disabled] button.btn.btn-inverse.active:hover, +fieldset[disabled] button.btn.btn-inverse.focus:hover, +fieldset[disabled] button.btn.btn-inverse:active:hover, +fieldset[disabled] button.btn.btn-inverse:focus:hover, +fieldset[disabled] button.btn.btn-inverse:hover, +fieldset[disabled] button.btn.btn-inverse:hover:hover { + -webkit-transform: none; + transform: none +} + +button.btn.btn-inverse.active, +button.btn.btn-inverse.focus, +button.btn.btn-inverse:active, +button.btn.btn-inverse:focus, +button.btn.btn-inverse:hover { + background: #949494; + border-color: #7c7c7c +} + +button.btn.btn-inverse:active, +button.btn.btn-inverse:hover, +button.btn.btn-inverse:target { + background-color: #949494; + color: #fff +} + +.btn-with-icon i { + margin-right: 10px +} + +.btn-group :hover, +.btn-toolbar :hover { + -webkit-transform: none; + transform: none +} + +.btn-group button.btn.btn-primary { + border-color: #149285 +} + +.btn-group button.btn.btn-primary:hover { + border-color: #088679 +} + +.btn-group button.btn.btn-danger { + border-color: #dc4a4a +} + +.btn-group button.btn.btn-danger:hover { + border-color: #d03e3e +} + +.btn-group button.btn.btn-info { + border-color: #21a0c5 +} + +.btn-group button.btn.btn-info:hover { + border-color: #1594b9 +} + +.btn-group button.btn.btn-success { + border-color: #84ad00 +} + +.btn-group button.btn.btn-success:hover { + border-color: #78a100 +} + +.btn-group button.btn.btn-warning { + border-color: #d3ac10 +} + +.btn-group button.btn.btn-warning:hover { + border-color: #c7a004 +} + +.btn-group .dropdown-menu { + margin-top: 0 +} + +.btn-toolbar { + display: inline-block +} + +.btn .caret { + margin-left: 2px +} + +button.progress-button .progress { + margin-bottom: 0; + border-radius: 0 +} + +button.progress-button:hover { + -webkit-transform: none; + transform: none +} + +button.progress-button.progress-button-style-shrink.btn.disabled.progress-button-dir-horizontal:hover { + -webkit-transform: scaleY(.3); + transform: scaleY(.3) +} + +button.progress-button.progress-button-style-shrink.btn.disabled.progress-button-dir-vertical:hover { + -webkit-transform: scaleX(.1); + transform: scaleX(.1) +} + +button.progress-button.btn.btn-primary .content:after, +button.progress-button.btn.btn-primary .content:before { + color: #000 +} + +button.progress-button.btn.btn-primary.progress-button-style-move-up .content, +button.progress-button.btn.btn-primary.progress-button-style-slide-down .content { + background-color: #17746a +} + +button.progress-button.btn.btn-primary.progress-button-style-lateral-lines .progress-inner { + border-color: #17746a; + background: 0 0 +} + +button.progress-button.btn.btn-primary .progress { + background-color: #17746a; + box-shadow: 0 1px 0 #17746a +} + +button.progress-button.btn.btn-primary .progress-inner { + background-color: #0f4943 +} + +button.progress-button.btn.btn-primary.progress-button-perspective { + background: 0 0 +} + +button.progress-button.btn.btn-primary.progress-button-perspective .content { + background-color: #209e91 +} + +button.progress-button.btn.btn-default .content:after, +button.progress-button.btn.btn-default .content:before { + color: #999 +} + +button.progress-button.btn.btn-default.progress-button-style-move-up .content, +button.progress-button.btn.btn-default.progress-button-style-slide-down .content { + background-color: #e6e6e6 +} + +button.progress-button.btn.btn-default.progress-button-style-lateral-lines .progress-inner { + border-color: #e6e6e6; + background: 0 0 +} + +button.progress-button.btn.btn-default .progress { + background-color: #e6e6e6; + box-shadow: 0 1px 0 #e6e6e6 +} + +button.progress-button.btn.btn-default .progress-inner { + background-color: #ccc +} + +button.progress-button.btn.btn-default.progress-button-perspective { + background: 0 0 +} + +button.progress-button.btn.btn-default.progress-button-perspective .content { + background-color: #fff +} + +button.progress-button.btn.btn-success .content:after, +button.progress-button.btn.btn-success .content:before { + color: #000 +} + +button.progress-button.btn.btn-success.progress-button-style-move-up .content, +button.progress-button.btn.btn-success.progress-button-style-slide-down .content { + background-color: #688600 +} + +button.progress-button.btn.btn-success.progress-button-style-lateral-lines .progress-inner { + border-color: #688600; + background: 0 0 +} + +button.progress-button.btn.btn-success .progress { + background-color: #688600; + box-shadow: 0 1px 0 #688600 +} + +button.progress-button.btn.btn-success .progress-inner { + background-color: #415300 +} + +button.progress-button.btn.btn-success.progress-button-perspective { + background: 0 0 +} + +button.progress-button.btn.btn-success.progress-button-perspective .content { + background-color: #90b900 +} + +button.progress-button.btn.btn-info .content:after, +button.progress-button.btn.btn-info .content:before { + color: #092229 +} + +button.progress-button.btn.btn-info.progress-button-style-move-up .content, +button.progress-button.btn.btn-info.progress-button-style-slide-down .content { + background-color: #2489a7 +} + +button.progress-button.btn.btn-info.progress-button-style-lateral-lines .progress-inner { + border-color: #2489a7; + background: 0 0 +} + +button.progress-button.btn.btn-info .progress { + background-color: #2489a7; + box-shadow: 0 1px 0 #2489a7 +} + +button.progress-button.btn.btn-info .progress-inner { + background-color: #1b677d +} + +button.progress-button.btn.btn-info.progress-button-perspective { + background: 0 0 +} + +button.progress-button.btn.btn-info.progress-button-perspective .content { + background-color: #2dacd1 +} + +button.progress-button.btn.btn-warning .content:after, +button.progress-button.btn.btn-warning .content:before { + color: #2a2205 +} + +button.progress-button.btn.btn-warning.progress-button-style-move-up .content, +button.progress-button.btn.btn-warning.progress-button-style-slide-down .content { + background-color: #b29316 +} + +button.progress-button.btn.btn-warning.progress-button-style-lateral-lines .progress-inner { + border-color: #b29316; + background: 0 0 +} + +button.progress-button.btn.btn-warning .progress { + background-color: #b29316; + box-shadow: 0 1px 0 #b29316 +} + +button.progress-button.btn.btn-warning .progress-inner { + background-color: #846d11 +} + +button.progress-button.btn.btn-warning.progress-button-perspective { + background: 0 0 +} + +button.progress-button.btn.btn-warning.progress-button-perspective .content { + background-color: #dfb81c +} + +button.progress-button.btn.btn-danger .content:after, +button.progress-button.btn.btn-danger .content:before { + color: #640e0e +} + +button.progress-button.btn.btn-danger.progress-button-style-move-up .content, +button.progress-button.btn.btn-danger.progress-button-style-slide-down .content { + background-color: #e22929 +} + +button.progress-button.btn.btn-danger.progress-button-style-lateral-lines .progress-inner { + border-color: #e22929; + background: 0 0 +} + +button.progress-button.btn.btn-danger .progress { + background-color: #e22929; + box-shadow: 0 1px 0 #e22929 +} + +button.progress-button.btn.btn-danger .progress-inner { + background-color: #be1a1a +} + +button.progress-button.btn.btn-danger.progress-button-perspective { + background: 0 0 +} + +button.progress-button.btn.btn-danger.progress-button-perspective .content { + background-color: #e85656 +} + +.btn-raised { + box-shadow: 0 2px 5px 0 rgba(0, 0, 0, .35) +} + +.btn-mm { + padding: 5px 11px; + font-size: 13px +} + +.btn-xm { + padding: 8px 14px; + font-size: 16px +} + +.dropdown button.btn.btn-default.dropdown-toggle { + color: #666; + border: 1px solid #d6d6d6; + background-color: transparent +} + +.dropdown button.btn.btn-default.dropdown-toggle:active, +.dropdown button.btn.btn-default.dropdown-toggle:focus { + background-color: #fff +} + +.bootstrap-select .dropdown-toggle:focus { + outline: 0!important +} + +.bootstrap-switch .bootstrap-switch-container:focus, +.bootstrap-switch:focus { + outline: 0 +} + +.bootstrap-select button.btn-default:focus { + color: #fff +} + +.bootstrap-select .btn { + -webkit-transition: none; + transition: none +} + +.label { + border-radius: 0 +} + +.label-primary { + background: #209e91 +} + +.label-info { + background: #63bbb2 +} + +.label-success { + background: #90b900 +} + +.label-warning { + background: #dfb81c +} + +.label-danger { + background: #e85656 +} + +.form-horizontal label { + line-height: 34px; + margin-bottom: 0; + padding-top: 0!important +} + +.form-group label { + margin-bottom: 5px; + color: #666; + font-weight: 400; + font-size: 13px +} + +.form-control { + border: 1px solid #cbcbcb; + background-color: #fff; + box-shadow: none +} + +.form-control::-webkit-input-placeholder { + color: #666; + opacity: .7 +} + +.form-control:-moz-placeholder { + color: #666; + opacity: .7 +} + +.form-control::-moz-placeholder { + color: #666; + opacity: .7 +} + +.form-control:-ms-input-placeholder { + color: #666; + opacity: .7 +} + +.form-control:focus { + box-shadow: none; + border-color: #4db1a7; + background: #fff +} + +select.form-control { + padding-left: 8px +} + +textarea.form-control { + height: 96px +} + +.form-inline .form-group input { + width: 100% +} + +.form-inline .form-group label { + margin-right: 12px +} + +.form-inline button[type=submit] { + margin-left: 12px +} + +.form-inline label.custom-checkbox>span, +.form-inline label.custom-radio>span { + display: block; + margin-top: -13px; + margin-right: 10px +} + +.switcher-container { + margin-right: 10px; + font-weight: 400 +} + +.switcher-container input { + display: none +} + +.switcher-container .switcher { + position: relative; + display: inline-block; + width: 84px; + overflow: hidden; + border-radius: 6px; + box-sizing: border-box; + -webkit-transform: translateZ(0) +} + +.switcher-container .switcher.primary { + border: 1px solid #209e91 +} + +.switcher-container .switcher.primary .handle.handle-on { + background-color: #209e91 +} + +.switcher-container .switcher.success { + border: 1px solid #90b900 +} + +.switcher-container .switcher.success .handle.handle-on { + background-color: #90b900 +} + +.switcher-container .switcher.warning { + border: 1px solid #dfb81c +} + +.switcher-container .switcher.warning .handle.handle-on { + background-color: #dfb81c +} + +.switcher-container .switcher.danger { + border: 1px solid #e85656 +} + +.switcher-container .switcher.danger .handle.handle-on { + background-color: #e85656 +} + +.switcher-container .switcher.info { + border: 1px solid #63bbb2 +} + +.switcher-container .switcher.info .handle.handle-on { + background-color: #63bbb2 +} + +.switcher-container .switcher input { + display: none +} + +.switcher-container .switcher .handle-container { + position: relative; + width: 126px; + cursor: pointer; + -webkit-transform: translate3d(-42px, 0, 0); + transform: translate3d(-42px, 0, 0); + -webkit-transition: -webkit-transform .2s linear; + transition: -webkit-transform .2s linear; + transition: transform .2s linear; + transition: transform .2s linear, -webkit-transform .2s linear +} + +.switcher-container .switcher .handle-container .handle { + width: 42px; + float: left; + line-height: 28px; + height: 28px; + font-size: 12px; + text-align: center; + color: #fff +} + +.switcher-container .switcher .handle-container .handle.handle-off { + background: #d6d6d6; + color: #000 +} + +.switcher-container input:checked+.switcher { + border: 1px solid #d6d6d6 +} + +.switcher-container input:checked+.switcher .handle-container { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0) +} + +.switch-container { + display: inline-block +} + +.switch-container.primary .bootstrap-switch.bootstrap-switch-on { + border-color: #209e91 +} + +.switch-container.success .bootstrap-switch.bootstrap-switch-on { + border-color: #90b900 +} + +.switch-container.warning .bootstrap-switch.bootstrap-switch-on { + border-color: #dfb81c +} + +.switch-container.danger .bootstrap-switch.bootstrap-switch-on { + border-color: #e85656 +} + +.switch-container.info .bootstrap-switch.bootstrap-switch-on { + border-color: #63bbb2 +} + +.bootstrap-switch { + border-radius: 5px; + border: 1px solid #fff; + -webkit-transition: border-color ease-in-out .7s, box-shadow ease-in-out .7s; + transition: border-color ease-in-out .7s, box-shadow ease-in-out .7s +} + +.bootstrap-switch.bootstrap-switch-focused.bootstrap-switch-off, +.bootstrap-switch.bootstrap-switch-off { + border-color: #d6d6d6 +} + +.bootstrap-switch .bootstrap-switch-container, +.bootstrap-switch .bootstrap-switch-handle-off, +.bootstrap-switch .bootstrap-switch-handle-on, +.input-group>span { + border-radius: 0 +} + +.bootstrap-switch.bootstrap-switch-focused { + box-shadow: none +} + +.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-default { + background: #fff +} + +.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-success { + background: #90b900 +} + +.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-primary { + background: #209e91 +} + +.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-warning { + background: #dfb81c +} + +.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-danger { + background: #e85656 +} + +.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-info { + background: #63bbb2 +} + +.bootstrap-switch .bootstrap-switch-label { + background: 0 0 +} + +.bootstrap-switch.bootstrap-switch-animate .bootstrap-switch-container { + -webkit-transition: margin-left .2s; + transition: margin-left .2s +} + +.switches { + margin-left: -12px; + margin-bottom: -12px +} + +.switches .switch-container { + float: left; + margin-left: 12px; + margin-bottom: 12px +} + +.input-group { + width: 100%; + margin-bottom: 15px +} + +label.custom-checkbox, +label.custom-radio { + padding-right: 0; + padding-left: 0; + margin-bottom: 0 +} + +label.custom-checkbox>input, +label.custom-radio>input { + height: 0; + z-index: -100!important; + opacity: 0; + position: absolute +} + +label.custom-checkbox>input:checked+span:before, +label.custom-radio>input:checked+span:before { + content: "\f00c"; + font-weight: 300 +} + +label.custom-checkbox>input:disabled+span, +label.custom-radio>input:disabled+span { + color: #ddd; + cursor: not-allowed +} + +label.custom-checkbox>input:disabled+span:before, +label.custom-radio>input:disabled+span:before { + border-color: #ddd!important; + cursor: not-allowed +} + +label.custom-checkbox>span, +label.custom-radio>span { + position: relative; + display: inline-block; + margin: 0; + line-height: 16px; + font-weight: 300; + cursor: pointer; + padding-left: 22px; + width: 100% +} + +label.custom-checkbox>span:before, +label.custom-radio>span:before { + cursor: pointer; + font-family: fontAwesome; + font-weight: 300; + font-size: 12px; + color: #666; + content: "\a0"; + background-color: transparent; + border: 1px solid #d6d6d6; + border-radius: 0; + display: inline-block; + text-align: center; + height: 16px; + line-height: 14px; + min-width: 16px; + margin-right: 6px; + position: relative; + top: 0; + margin-left: -22px; + float: left +} + +.auth-main, +body, +html { + height: 100% +} + +label.custom-checkbox>span:hover:before, +label.custom-radio>span:hover:before { + border-color: #4db1a7 +} + +.nowrap { + white-space: nowrap +} + +.cut-with-dots { + overflow: hidden; + text-overflow: ellipsis; + display: block +} + +label.custom-radio>input:checked+span:before { + content: "\f111" +} + +label.custom-radio>span:before { + border-radius: 16px; + font-size: 9px +} + +label.custom-input-primary>span:before { + color: #209e91 +} + +label.custom-input-primary>span:hover:before { + border-color: #209e91 +} + +label.custom-input-success>span:before { + color: #90b900 +} + +label.custom-input-success>span:hover:before { + border-color: #90b900 +} + +label.custom-input-warning>span:before { + color: #dfb81c +} + +label.custom-input-warning>span:hover:before { + border-color: #dfb81c +} + +label.custom-input-danger>span:before { + color: #e85656 +} + +label.custom-input-danger>span:hover:before { + border-color: #e85656 +} + +.form-horizontal .checkbox, +.form-horizontal .checkbox-inline, +.form-horizontal .radio, +.form-horizontal .radio-inline { + padding-top: 0 +} + +.input-demo { + line-height: 25px +} + +.has-success .control-label { + color: #666 +} + +.has-success .form-control-feedback, +.has-success label.custom-checkbox, +.has-success label.custom-checkbox>span:before, +.has-success label.custom-radio, +.has-success label.custom-radio>span:before { + color: #a6c733 +} + +.has-success .form-control { + border: 1px solid #a6c733 +} + +.has-success .form-control:focus { + box-shadow: none; + border-color: #90b900 +} + +.has-success label.custom-checkbox>span:hover:before, +.has-success label.custom-radio>span:hover:before { + border-color: #a6c733 +} + +.has-success .input-group-addon { + background-color: #a6c733; + color: #fff +} + +.has-warning .control-label { + color: #666 +} + +.has-warning .form-control-feedback, +.has-warning label.custom-checkbox, +.has-warning label.custom-checkbox>span:before, +.has-warning label.custom-radio, +.has-warning label.custom-radio>span:before { + color: #e5c649 +} + +.has-warning .form-control { + border: 1px solid #e5c649 +} + +.has-warning .form-control:focus { + box-shadow: none; + border-color: #dfb81c +} + +.has-warning label.custom-checkbox>span:hover:before, +.has-warning label.custom-radio>span:hover:before { + border-color: #e5c649 +} + +.has-warning .input-group-addon { + background-color: #e5c649; + color: #fff +} + +.has-error .control-label { + color: #666 +} + +.has-error .form-control-feedback, +.has-error label.custom-checkbox, +.has-error label.custom-checkbox>span:before, +.has-error label.custom-radio, +.has-error label.custom-radio>span:before { + color: #ed7878 +} + +.has-error .form-control { + border: 1px solid #ed7878 +} + +.has-error .form-control:focus { + box-shadow: none; + border-color: #e85656 +} + +.has-error label.custom-checkbox>span:hover:before, +.has-error label.custom-radio>span:hover:before { + border-color: #ed7878 +} + +.has-error .input-group-addon { + background-color: #ed7878; + color: #fff +} + +.has-feedback label~.form-control-feedback { + top: 21px; + font-size: 18px +} + +.bootstrap-select .btn-default:focus { + color: #666 +} + +.bootstrap-select>.dropdown-toggle.btn-danger, +.bootstrap-select>.dropdown-toggle.btn-danger:focus, +.bootstrap-select>.dropdown-toggle.btn-danger:hover, +.bootstrap-select>.dropdown-toggle.btn-info, +.bootstrap-select>.dropdown-toggle.btn-info:focus, +.bootstrap-select>.dropdown-toggle.btn-info:hover, +.bootstrap-select>.dropdown-toggle.btn-inverse, +.bootstrap-select>.dropdown-toggle.btn-inverse:focus, +.bootstrap-select>.dropdown-toggle.btn-inverse:hover, +.bootstrap-select>.dropdown-toggle.btn-primary, +.bootstrap-select>.dropdown-toggle.btn-primary:focus, +.bootstrap-select>.dropdown-toggle.btn-primary:hover, +.bootstrap-select>.dropdown-toggle.btn-success, +.bootstrap-select>.dropdown-toggle.btn-success:focus, +.bootstrap-select>.dropdown-toggle.btn-success:hover, +.bootstrap-select>.dropdown-toggle.btn-warning, +.bootstrap-select>.dropdown-toggle.btn-warning:focus, +.bootstrap-select>.dropdown-toggle.btn-warning:hover { + color: #fff +} + +.form-control[disabled], +.form-control[readonly], +fieldset[disabled] .form-control { + background-color: #fff; + color: #ddd; + border-color: #e2e2e2 +} + +.form-control-rounded { + border-radius: 16px +} + +.help-block { + color: #949494 +} + +.help-block.error-block { + display: none +} + +.has-error .help-block.error-block.basic-block { + display: block +} + +.input-group-addon-danger { + background: #e85656; + color: #fff; + border-color: #e85656 +} + +.input-group-addon-warning { + background: #dfb81c; + color: #fff; + border-color: #dfb81c +} + +.input-group-addon-success { + background: #90b900; + color: #fff; + border-color: #90b900 +} + +.input-group-addon-primary { + background: #209e91; + color: #fff; + border-color: #209e91 +} + +.checkbox-demo-row { + margin-bottom: 12px +} + +.dropdown-menu { + border-radius: 5px +} + +.bootstrap-select.btn-group button.btn.btn-default { + background: 0 0; + color: #666 +} + +.bootstrap-select.btn-group button.btn.btn-default:hover { + background: #fff; + box-shadow: none; + outline: 0!important +} + +.bootstrap-select.btn-group button.btn.btn-default:active { + background: #fff; + box-shadow: none +} + +.bootstrap-select.btn-group.open>.btn.btn-default.dropdown-toggle { + background: #fff; + box-shadow: none; + border-color: #d6d6d6 +} + +.bootstrap-select.btn-group.open>.btn { + border-radius: 5px 5px 0 0 +} + +.bootstrap-select.btn-group.open .dropdown-menu.open { + border: 1px solid #dcdcdc; + border-top: none; + border-radius: 0 0 5px 5px +} + +.bootstrap-select.btn-group.with-search.open .btn-default+.dropdown-menu .bs-searchbox .form-control, +.bootstrap-tagsinput { + border: 1px solid #cbcbcb; + background-color: #fff +} + +.bootstrap-select.btn-group .notify, +.bootstrap-select.btn-group.with-search.open .btn-default+.dropdown-menu .no-results { + color: #7d7d7d +} + +.bootstrap-tagsinput { + color: #666; + border-radius: 5px; + box-shadow: none; + max-width: 100%; + font-size: 14px; + line-height: 26px; + width: 100% +} + +.bootstrap-tagsinput.form-control { + display: block; + width: 100% +} + +.bootstrap-tagsinput .tag { + border-radius: 3px; + font-weight: 400; + font-size: 11px; + padding: 4px 8px +} + +.bootstrap-tagsinput .tag [data-role=remove]:hover { + box-shadow: none +} + +.bootstrap-tagsinput input { + background-color: #fff; + border: 1px solid #cbcbcb; + border-radius: 5px; + line-height: 22px; + font-size: 11px; + min-width: 53px +} + +.bootstrap-tagsinput input::-webkit-input-placeholder { + color: #666; + opacity: .8 +} + +.bootstrap-tagsinput input:-moz-placeholder { + color: #666; + opacity: .8 +} + +.bootstrap-tagsinput input::-moz-placeholder { + color: #666; + opacity: .8 +} + +.bootstrap-tagsinput input:-ms-input-placeholder { + color: #666; + opacity: .8 +} + +.ui-select-multiple.ui-select-bootstrap { + min-height: 34px; + padding: 4px 3px 0 12px +} + +.progress { + background: rgba(0, 0, 0, .07) +} + +.progress-bar-primary { + background-color: #209e91 +} + +.progress-bar-success { + background-color: #b1ce4d +} + +.progress-bar-warning { + background-color: #dfb81c +} + +.progress-bar-danger { + background-color: #e85656 +} + +.has-success .input-group-addon { + border: none +} + +.input-group>span.addon-left { + border-top-left-radius: 5px; + border-bottom-left-radius: 5px +} + +.input-group>span.addon-right { + border-top-right-radius: 5px; + border-bottom-right-radius: 5px +} + +.with-primary-addon:focus { + border-color: #209e91 +} + +.with-warning-addon:focus { + border-color: #dfb81c +} + +.with-success-addon:focus { + border-color: #90b900 +} + +.with-danger-addon:focus { + border-color: #e85656 +} + +.sub-little-text { + font-size: 12px +} + +.auth-block p, +.auth-link, +.auth-sep { + font-size: 16px +} + +html { + min-height: 520px +} + +.form-control, +.form-control:focus { + background-color: rgba(0, 0, 0, .4); + border-radius: 5px; + color: #fff +} + +.form-control::-webkit-input-placeholder, +.form-control:focus::-webkit-input-placeholder { + color: #fff; + opacity: .9 +} + +.form-control:-moz-placeholder, +.form-control:focus:-moz-placeholder { + color: #fff; + opacity: .9 +} + +.form-control::-moz-placeholder, +.form-control:focus::-moz-placeholder { + color: #fff; + opacity: .9 +} + +.form-control:-ms-input-placeholder, +.form-control:focus:-ms-input-placeholder { + color: #fff; + opacity: .9 +} + +.form-control[disabled]::-webkit-input-placeholder, +.form-control[readonly]::-webkit-input-placeholder, +fieldset[disabled] .form-control::-webkit-input-placeholder { + color: #fff; + opacity: .6 +} + +.form-control[disabled]:-moz-placeholder, +.form-control[readonly]:-moz-placeholder, +fieldset[disabled] .form-control:-moz-placeholder { + color: #fff; + opacity: .6 +} + +.form-control[disabled]::-moz-placeholder, +.form-control[readonly]::-moz-placeholder, +fieldset[disabled] .form-control::-moz-placeholder { + color: #fff; + opacity: .6 +} + +.form-control[disabled]:-ms-input-placeholder, +.form-control[readonly]:-ms-input-placeholder, +fieldset[disabled] .form-control:-ms-input-placeholder { + color: #fff; + opacity: .6 +} + +.auth-main { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + width: 100%; + content: ''; + position: fixed; + width: 100%; + height: 100%; + top: 0; + left: 0; + background: url(../../static/img/blur-bg.jpg) center center no-repeat; + background-size: cover; + will-change: transform; + z-index: -1 +} + +.auth-block { + width: 540px; + margin: 0 auto; + border-radius: 5px; + background: rgba(0, 0, 0, .55); + color: #fff; + padding: 32px +} + +.auth-block h1 { + font-weight: 300; + margin-bottom: 28px; + text-align: center +} + +.auth-block a { + text-decoration: none; + outline: 0; + -webkit-transition: all .2s ease; + transition: all .2s ease; + color: #209e91 +} + +.auth-block a:hover { + color: #1b867b +} + +.auth-block .control-label { + padding-top: 11px; + color: #fff +} + +.auth-block .form-group { + margin-bottom: 12px +} + +.auth-input { + width: 300px; + margin-bottom: 24px +} + +.auth-input input { + display: block; + width: 100%; + border: none; + font-size: 16px; + padding: 4px 10px; + outline: 0 +} + +a.forgot-pass { + display: block; + text-align: right; + margin-bottom: -20px; + float: right; + z-index: 2; + position: relative +} + +.al-share-auth, +.auth-link, +.auth-sep { + text-align: center +} + +.auth-link { + display: block; + margin-bottom: 33px +} + +.auth-sep { + margin-top: 36px; + margin-bottom: 24px; + line-height: 20px; + display: block; + position: relative +} + +.auth-sep>span { + display: table-cell; + width: 30%; + white-space: nowrap; + padding: 0 24px; + color: #fff +} + +.auth-sep>span>span { + margin-top: -12px; + display: block +} + +.auth-sep:after, +.auth-sep:before { + border-top: solid 1px #fff; + content: ""; + height: 1px; + width: 35%; + display: table-cell +} + +.al-share-auth .al-share { + float: none; + margin: 0; + padding: 0; + display: inline-block +} + +.al-share-auth .al-share li { + margin-left: 24px +} + +.al-share-auth .al-share li:first-child { + margin-left: 0 +} + +.al-share-auth .al-share li i { + font-size: 24px +} + +.btn-auth { + color: #fff!important +} + + +/*# sourceMappingURL=../maps/styles/auth-a200a050c1.css.map */
\ No newline at end of file diff --git a/static/img/blur-bg.jpg b/static/img/blur-bg.jpg Binary files differnew file mode 100644 index 0000000000..3f557e85b6 --- /dev/null +++ b/static/img/blur-bg.jpg diff --git a/static/index.html b/static/index.html index de7f47cce5..6e5f9bdbb2 100644 --- a/static/index.html +++ b/static/index.html @@ -17,21 +17,24 @@ <script type="text/javascript" src="static/js/angular-animate.js" defer></script> <script type="text/javascript" src="static/js/angular-sanitize.js" defer></script> <script type="text/javascript" src="static/js/angular-cookies.js" defer></script> - <script type="text/javascript" src="static/js/angular-resource.js" defer></script> <script type="text/javascript" src="static/js/angular-ui-router.js" defer></script> <script type="text/javascript" src="static/js/angular-websocket.js" defer></script> + <script type="text/javascript" src="static/js/angular-ui-router-uib-modal.js" defer></script> <script type="text/javascript" src="static/js/lodash.core.js" defer></script> - <script type="text/javascript" src="static/js/ui-bootstrap-tpls-2.1.3.js" defer></script> + <script type="text/javascript" src="static/js/ui-bootstrap-tpls-2.5.0.js" defer></script> <script type="text/javascript" src="static/js/bmcApp.js" defer></script> <script type="text/javascript" src="static/js/base64.js" defer></script> + <script type="text/javascript" src="static/js/mainController.js" defer></script> + <script type="text/javascript" src="static/js/versionController.js" defer></script> <script type="text/javascript" src="static/js/selController.js" defer></script> <script type="text/javascript" src="static/js/loginController.js" defer></script> <script type="text/javascript" src="static/js/kvmController.js" defer></script> <script type="text/javascript" src="static/js/ipmiController.js" defer></script> <script type="text/javascript" src="static/js/sensorController.js" defer></script> + <script type="text/javascript" src="static/js/fwupdateController.js" defer></script> <script type="text/javascript" src="static/noVNC/core/util.js" defer></script> <script type="text/javascript" src="static/noVNC/app/webutil.js" defer></script> @@ -50,14 +53,10 @@ </head> -<body> - <div class="div-fake-hidden"><i class="fa fa-square-o fa-3x"></i></div> - <div ng-controller="MainCtrl"> - <nav class="navbar navbar-inverse"> - - +<body ng-controller="MainCtrl" ng-class="(is_logged_in()) ? '' : 'auth-main'"> + <div> + <nav class="navbar navbar-inverse" ng-if='is_logged_in()'> <div class="container-fluid"> - <!-- Brand and toggle get grouped for better mobile display --> <div class="navbar-header navbar-left"> <a class="navbar-brand" href="#"><img style="max-width:100%; max-height:100%;" src="static/img/logo.png" /></a> @@ -98,7 +97,7 @@ <li><a href="#">IPv6 Network</a></li> <li><a href="#">VLAN</a></li> <li><a href="#">KVM & Media</a></li> - <li><a href="#">SSL Certification</a></li> + <li><a href="#">SSL Certikfication</a></li> <li><a href="#">Users</a></li> <li><a href="#">Security Settings</a></li> <li><a href="#">SOL</a></li> @@ -136,10 +135,9 @@ </div> </nav> - <!-- MAIN CONTENT --> - <div class="container"> - <div ui-view></div> - </div> + + <div ui-view ng-class="(is_logged_in()) ? '' : 'auth-main'"></div> + </div> </body> diff --git a/static/js/angular-animate.js b/static/js/angular-animate.js index b18b828ebf..121a9ef8d8 100644 --- a/static/js/angular-animate.js +++ b/static/js/angular-animate.js @@ -1,6 +1,6 @@ /** - * @license AngularJS v1.5.8 - * (c) 2010-2016 Google, Inc. http://angularjs.org + * @license AngularJS v1.6.4 + * (c) 2010-2017 Google, Inc. http://angularjs.org * License: MIT */ (function(window, angular) {'use strict'; @@ -29,7 +29,7 @@ var CSS_PREFIX = '', TRANSITION_PROP, TRANSITIONEND_EVENT, ANIMATION_PROP, ANIMA // Also, the only modern browser that uses vendor prefixes for transitions/keyframes is webkit // therefore there is no reason to test anymore for other vendor prefixes: // http://caniuse.com/#search=transition -if ((window.ontransitionend === void 0) && (window.onwebkittransitionend !== void 0)) { +if ((window.ontransitionend === undefined) && (window.onwebkittransitionend !== undefined)) { CSS_PREFIX = '-webkit-'; TRANSITION_PROP = 'WebkitTransition'; TRANSITIONEND_EVENT = 'webkitTransitionEnd transitionend'; @@ -38,7 +38,7 @@ if ((window.ontransitionend === void 0) && (window.onwebkittransitionend !== voi TRANSITIONEND_EVENT = 'transitionend'; } -if ((window.onanimationend === void 0) && (window.onwebkitanimationend !== void 0)) { +if ((window.onanimationend === undefined) && (window.onwebkitanimationend !== undefined)) { CSS_PREFIX = '-webkit-'; ANIMATION_PROP = 'WebkitAnimation'; ANIMATIONEND_EVENT = 'webkitAnimationEnd animationend'; @@ -63,7 +63,7 @@ var TRANSITION_DURATION_PROP = TRANSITION_PROP + DURATION_KEY; var ngMinErr = angular.$$minErr('ng'); function assertArg(arg, name, reason) { if (!arg) { - throw ngMinErr('areq', "Argument '{0}' is {1}", (name || '?'), (reason || "required")); + throw ngMinErr('areq', 'Argument \'{0}\' is {1}', (name || '?'), (reason || 'required')); } return arg; } @@ -139,7 +139,7 @@ function extractElementNode(element) { if (!element[0]) return element; for (var i = 0; i < element.length; i++) { var elm = element[i]; - if (elm.nodeType == ELEMENT_NODE) { + if (elm.nodeType === ELEMENT_NODE) { return elm; } } @@ -423,7 +423,7 @@ var $$rAFSchedulerFactory = ['$$rAF', function($$rAF) { * of the children's parents are currently animating. By default, when an element has an active `enter`, `leave`, or `move` * (structural) animation, child elements that also have an active structural animation are not animated. * - * Note that even if `ngAnimteChildren` is set, no child animations will run when the parent element is removed from the DOM (`leave` animation). + * Note that even if `ngAnimateChildren` is set, no child animations will run when the parent element is removed from the DOM (`leave` animation). * * * @param {string} ngAnimateChildren If the value is empty, `true` or `on`, @@ -432,7 +432,7 @@ var $$rAFSchedulerFactory = ['$$rAF', function($$rAF) { * @example * <example module="ngAnimateChildren" name="ngAnimateChildren" deps="angular-animate.js" animations="true"> <file name="index.html"> - <div ng-controller="mainController as main"> + <div ng-controller="MainController as main"> <label>Show container? <input type="checkbox" ng-model="main.enterElement" /></label> <label>Animate children? <input type="checkbox" ng-model="main.animateChildren" /></label> <hr> @@ -482,7 +482,7 @@ var $$rAFSchedulerFactory = ['$$rAF', function($$rAF) { </file> <file name="script.js"> angular.module('ngAnimateChildren', ['ngAnimate']) - .controller('mainController', function() { + .controller('MainController', function MainController() { this.animateChildren = false; this.enterElement = false; }); @@ -510,6 +510,8 @@ var $$AnimateChildrenDirective = ['$interpolate', function($interpolate) { }; }]; +/* exported $AnimateCssProvider */ + var ANIMATE_TIMER_KEY = '$$animateCss'; /** @@ -727,7 +729,6 @@ var ANIMATE_TIMER_KEY = '$$animateCss'; * * `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; @@ -789,7 +790,7 @@ function parseMaxTime(str) { 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') { + if (value.charAt(value.length - 1) === 's') { value = value.substring(0, value.length - 1); } value = parseFloat(value) || 0; @@ -857,7 +858,7 @@ function registerRestorableStyles(backup, node, properties) { }); } -var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { +var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animateProvider) { var gcsLookup = createLocalCacheLookup(); var gcsStaggerLookup = createLocalCacheLookup(); @@ -870,7 +871,7 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { var parentCounter = 0; function gcsHashFn(node, extraClasses) { - var KEY = "$$ngAnimateParentKey"; + var KEY = '$$ngAnimateParentKey'; var parentNode = node.parentNode; var parentID = parentNode[KEY] || (parentNode[KEY] = ++parentCounter); return parentID + '-' + node.getAttribute('class') + '-' + extraClasses; @@ -921,7 +922,6 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { return stagger || {}; } - var cancelLastRAFRequest; var rafWaitQueue = []; function waitUntilQuiet(callback) { rafWaitQueue.push(callback); @@ -1110,7 +1110,7 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { var flags = {}; flags.hasTransitions = timings.transitionDuration > 0; flags.hasAnimations = timings.animationDuration > 0; - flags.hasTransitionAll = flags.hasTransitions && timings.transitionProperty == 'all'; + flags.hasTransitionAll = flags.hasTransitions && timings.transitionProperty === 'all'; flags.applyTransitionDuration = hasToStyles && ( (flags.hasTransitions && !flags.hasTransitionAll) || (flags.hasAnimations && !flags.hasTransitions)); @@ -1142,7 +1142,7 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { if (options.delay != null) { var delayStyle; - if (typeof options.delay !== "boolean") { + 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); @@ -1220,7 +1220,7 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { close(true); } - function close(rejected) { // jshint ignore:line + function close(rejected) { // if the promise has been called already then we shouldn't close // the animation again if (animationClosed || (animationCompleted && animationPaused)) return; @@ -1247,8 +1247,11 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { if (Object.keys(restoreStyles).length) { forEach(restoreStyles, function(value, prop) { - value ? node.style.setProperty(prop, value) - : node.style.removeProperty(prop); + if (value) { + node.style.setProperty(prop, value); + } else { + node.style.removeProperty(prop); + } }); } @@ -1351,9 +1354,11 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { animationPaused = !playAnimation; if (timings.animationDuration) { var value = blockKeyframeAnimations(node, animationPaused); - animationPaused - ? temporaryStyles.push(value) - : removeFromArray(temporaryStyles, value); + if (animationPaused) { + temporaryStyles.push(value); + } else { + removeFromArray(temporaryStyles, value); + } } } else if (animationPaused && playAnimation) { animationPaused = false; @@ -1402,7 +1407,7 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { $$jqLite.addClass(element, activeClasses); if (flags.recalculateTimingStyles) { - fullClassName = node.className + ' ' + preparationClasses; + fullClassName = node.getAttribute('class') + ' ' + preparationClasses; cacheKey = gcsHashFn(node, fullClassName); timings = computeTimings(node, fullClassName, cacheKey); @@ -1420,7 +1425,7 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { } if (flags.applyAnimationDelay) { - relativeDelay = typeof options.delay !== "boolean" && truthyTimingValue(options.delay) + relativeDelay = typeof options.delay !== 'boolean' && truthyTimingValue(options.delay) ? parseFloat(options.delay) : relativeDelay; @@ -1512,7 +1517,7 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { }]; }]; -var $$AnimateCssDriverProvider = ['$$animationProvider', function($$animationProvider) { +var $$AnimateCssDriverProvider = ['$$animationProvider', /** @this */ function($$animationProvider) { $$animationProvider.drivers.push('$$animateCssDriver'); var NG_ANIMATE_SHIM_CLASS_NAME = 'ng-animate-shim'; @@ -1541,8 +1546,6 @@ var $$AnimateCssDriverProvider = ['$$animationProvider', function($$animationPro isDocumentFragment(rootNode) || bodyNode.contains(rootNode) ? rootNode : bodyNode ); - var applyAnimationClasses = applyAnimationClassesFactory($$jqLite); - return function initDriverFn(animationDetails) { return animationDetails.from && animationDetails.to ? prepareFromToAnchorAnimation(animationDetails.from, @@ -1784,7 +1787,7 @@ var $$AnimateCssDriverProvider = ['$$animationProvider', function($$animationPro // TODO(matsko): add documentation // by the time... -var $$AnimateJsProvider = ['$animateProvider', function($animateProvider) { +var $$AnimateJsProvider = ['$animateProvider', /** @this */ function($animateProvider) { this.$get = ['$injector', '$$AnimateRunner', '$$jqLite', function($injector, $$AnimateRunner, $$jqLite) { @@ -1823,7 +1826,7 @@ var $$AnimateJsProvider = ['$animateProvider', function($animateProvider) { var before, after; if (animations.length) { var afterFn, beforeFn; - if (event == 'leave') { + if (event === 'leave') { beforeFn = 'leave'; afterFn = 'afterLeave'; // TODO(matsko): get rid of this } else { @@ -2008,7 +2011,7 @@ var $$AnimateJsProvider = ['$animateProvider', function($animateProvider) { function packageAnimations(element, event, options, animations, fnName) { var operations = groupEventedAnimations(element, event, options, animations, fnName); if (operations.length === 0) { - var a,b; + var a, b; if (fnName === 'beforeSetClass') { a = groupEventedAnimations(element, 'removeClass', options, animations, 'beforeRemoveClass'); b = groupEventedAnimations(element, 'addClass', options, animations, 'beforeAddClass'); @@ -2036,11 +2039,19 @@ var $$AnimateJsProvider = ['$animateProvider', function($animateProvider) { }); } - runners.length ? $$AnimateRunner.all(runners, callback) : callback(); + if (runners.length) { + $$AnimateRunner.all(runners, callback); + } else { + callback(); + } return function endFn(reject) { forEach(runners, function(runner) { - reject ? runner.cancel() : runner.end(); + if (reject) { + runner.cancel(); + } else { + runner.end(); + } }); }; }; @@ -2050,7 +2061,7 @@ var $$AnimateJsProvider = ['$animateProvider', function($animateProvider) { function lookupAnimations(classes) { classes = isArray(classes) ? classes : classes.split(' '); var matches = [], flagMap = {}; - for (var i=0; i < classes.length; i++) { + for (var i = 0; i < classes.length; i++) { var klass = classes[i], animationFactory = $animateProvider.$$registeredAnimations[klass]; if (animationFactory && !flagMap[klass]) { @@ -2063,7 +2074,7 @@ var $$AnimateJsProvider = ['$animateProvider', function($animateProvider) { }]; }]; -var $$AnimateJsDriverProvider = ['$$animationProvider', function($$animationProvider) { +var $$AnimateJsDriverProvider = ['$$animationProvider', /** @this */ function($$animationProvider) { $$animationProvider.drivers.push('$$animateJsDriver'); this.$get = ['$$animateJs', '$$AnimateRunner', function($$animateJs, $$AnimateRunner) { return function initDriverFn(animationDetails) { @@ -2125,7 +2136,7 @@ var $$AnimateJsDriverProvider = ['$$animationProvider', function($$animationProv var NG_ANIMATE_ATTR_NAME = 'data-ng-animate'; var NG_ANIMATE_PIN_DATA = '$ngAnimatePin'; -var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { +var $$AnimateQueueProvider = ['$animateProvider', /** @this */ function($animateProvider) { var PRE_DIGEST_STATE = 1; var RUNNING_STATE = 2; var ONE_SPACE = ' '; @@ -2159,9 +2170,9 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { } } - function isAllowed(ruleType, element, currentAnimation, previousAnimation) { + function isAllowed(ruleType, currentAnimation, previousAnimation) { return rules[ruleType].some(function(fn) { - return fn(element, currentAnimation, previousAnimation); + return fn(currentAnimation, previousAnimation); }); } @@ -2171,40 +2182,40 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { return and ? a && b : a || b; } - rules.join.push(function(element, newAnimation, currentAnimation) { + rules.join.push(function(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) { + rules.skip.push(function(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) { + rules.skip.push(function(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; + return currentAnimation.event === 'leave' && newAnimation.structural; }); - rules.skip.push(function(element, newAnimation, currentAnimation) { + rules.skip.push(function(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) { + rules.cancel.push(function(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) { + rules.cancel.push(function(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) { + rules.cancel.push(function(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 @@ -2223,13 +2234,15 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { return hasMatchingClasses(nA, cR) || hasMatchingClasses(nR, cA); }); - this.$get = ['$$rAF', '$rootScope', '$rootElement', '$document', '$$HashMap', + this.$get = ['$$rAF', '$rootScope', '$rootElement', '$document', '$$Map', '$$animation', '$$AnimateRunner', '$templateRequest', '$$jqLite', '$$forceReflow', - function($$rAF, $rootScope, $rootElement, $document, $$HashMap, - $$animation, $$AnimateRunner, $templateRequest, $$jqLite, $$forceReflow) { + '$$isDocumentHidden', + function($$rAF, $rootScope, $rootElement, $document, $$Map, + $$animation, $$AnimateRunner, $templateRequest, $$jqLite, $$forceReflow, + $$isDocumentHidden) { - var activeAnimationsLookup = new $$HashMap(); - var disabledElementsLookup = new $$HashMap(); + var activeAnimationsLookup = new $$Map(); + var disabledElementsLookup = new $$Map(); var animationsEnabled = null; function postDigestTaskFactory() { @@ -2297,16 +2310,12 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { } // 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 + var contains = window.Node.prototype.contains || /** @this */ function(arg) { + // eslint-disable-next-line no-bitwise return this === arg || !!(this.compareDocumentPosition(arg) & 16); - // jshint bitwise: true }; - function findCallbacks(parent, element, event) { - var targetNode = getDomNode(element); - var targetParentNode = getDomNode(parent); - + function findCallbacks(targetParentNode, targetNode, event) { var matches = []; var entries = callbackRegistry[event]; if (entries) { @@ -2331,11 +2340,11 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { }); } - function cleanupEventListeners(phase, element) { - if (phase === 'close' && !element[0].parentNode) { + function cleanupEventListeners(phase, node) { + if (phase === 'close' && !node.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); + $animate.off(node); } } @@ -2416,7 +2425,7 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { bool = !disabledElementsLookup.get(node); } else { // (element, bool) - Element setter - disabledElementsLookup.put(node, !bool); + disabledElementsLookup.set(node, !bool); } } } @@ -2427,18 +2436,15 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { return $animate; - function queueAnimation(element, event, initialOptions) { + function queueAnimation(originalElement, 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(); - } + var element = stripCommentsFromElement(originalElement); + var node = getDomNode(element); + var parentNode = node && node.parentNode; options = prepareAnimationOptions(options); @@ -2481,7 +2487,7 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { return runner; } - var className = [node.className, options.addClass, options.removeClass].join(' '); + var className = [node.getAttribute('class'), options.addClass, options.removeClass].join(' '); if (!isAnimatableClassName(className)) { close(); return runner; @@ -2489,7 +2495,7 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { var isStructural = ['enter', 'move', 'leave'].indexOf(event) >= 0; - var documentHidden = $document[0].hidden; + var documentHidden = $$isDocumentHidden(); // this is a hard disable of all animations for the application or on // the element itself, therefore there is no need to continue further @@ -2502,8 +2508,8 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { // 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 && (!hasExistingAnimation || existingAnimation.state !== PRE_DIGEST_STATE)) { + skipAnimations = !areAnimationsAllowed(node, parentNode, event); } if (skipAnimations) { @@ -2515,7 +2521,7 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { } if (isStructural) { - closeChildAnimations(element); + closeChildAnimations(node); } var newAnimation = { @@ -2530,7 +2536,7 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { }; if (hasExistingAnimation) { - var skipAnimationFlag = isAllowed('skip', element, newAnimation, existingAnimation); + var skipAnimationFlag = isAllowed('skip', newAnimation, existingAnimation); if (skipAnimationFlag) { if (existingAnimation.state === RUNNING_STATE) { close(); @@ -2540,7 +2546,7 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { return existingAnimation.runner; } } - var cancelAnimationFlag = isAllowed('cancel', element, newAnimation, existingAnimation); + var cancelAnimationFlag = isAllowed('cancel', newAnimation, existingAnimation); if (cancelAnimationFlag) { if (existingAnimation.state === RUNNING_STATE) { // this will end the animation right away and it is safe @@ -2562,7 +2568,7 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { // 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); + var joinAnimationFlag = isAllowed('join', newAnimation, existingAnimation); if (joinAnimationFlag) { if (existingAnimation.state === RUNNING_STATE) { normalizeAnimationDetails(element, newAnimation); @@ -2596,7 +2602,7 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { if (!isValidAnimation) { close(); - clearElementAnimationState(element); + clearElementAnimationState(node); return runner; } @@ -2604,9 +2610,18 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { var counter = (existingAnimation.counter || 0) + 1; newAnimation.counter = counter; - markElementAnimationState(element, PRE_DIGEST_STATE, newAnimation); + markElementAnimationState(node, PRE_DIGEST_STATE, newAnimation); $rootScope.$$postDigest(function() { + // It is possible that the DOM nodes inside `originalElement` have been replaced. This can + // happen if the animated element is a transcluded clone and also has a `templateUrl` + // directive on it. Therefore, we must recreate `element` in order to interact with the + // actual DOM nodes. + // Note: We still need to use the old `node` for certain things, such as looking up in + // HashMaps where it was used as the key. + + element = stripCommentsFromElement(originalElement); + var animationDetails = activeAnimationsLookup.get(node); var animationCancelled = !animationDetails; animationDetails = animationDetails || {}; @@ -2645,7 +2660,7 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { // 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); + clearElementAnimationState(node); } return; @@ -2657,7 +2672,7 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { ? 'setClass' : animationDetails.event; - markElementAnimationState(element, RUNNING_STATE); + markElementAnimationState(node, RUNNING_STATE); var realRunner = $$animation(element, event, animationDetails.options); // this will update the runner's flow-control events based on @@ -2669,7 +2684,7 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { close(!status); var animationDetails = activeAnimationsLookup.get(node); if (animationDetails && animationDetails.counter === counter) { - clearElementAnimationState(getDomNode(element)); + clearElementAnimationState(node); } notifyProgress(runner, event, 'close', {}); }); @@ -2679,7 +2694,7 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { function notifyProgress(runner, event, phase, data) { runInNextPostDigestOrNow(function() { - var callbacks = findCallbacks(parent, element, event); + var callbacks = findCallbacks(parentNode, node, event); if (callbacks.length) { // do not optimize this call here to RAF because // we don't know how heavy the callback code here will @@ -2689,16 +2704,16 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { forEach(callbacks, function(callback) { callback(element, phase, data); }); - cleanupEventListeners(phase, element); + cleanupEventListeners(phase, node); }); } else { - cleanupEventListeners(phase, element); + cleanupEventListeners(phase, node); } }); runner.progress(event, phase, data); } - function close(reject) { // jshint ignore:line + function close(reject) { clearGeneratedClasses(element, options); applyAnimationClasses(element, options); applyAnimationStyles(element, options); @@ -2707,11 +2722,10 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { } } - function closeChildAnimations(element) { - var node = getDomNode(element); + function closeChildAnimations(node) { var children = node.querySelectorAll('[' + NG_ANIMATE_ATTR_NAME + ']'); forEach(children, function(child) { - var state = parseInt(child.getAttribute(NG_ANIMATE_ATTR_NAME)); + var state = parseInt(child.getAttribute(NG_ANIMATE_ATTR_NAME), 10); var animationDetails = activeAnimationsLookup.get(child); if (animationDetails) { switch (state) { @@ -2719,21 +2733,16 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { animationDetails.runner.end(); /* falls through */ case PRE_DIGEST_STATE: - activeAnimationsLookup.remove(child); + activeAnimationsLookup.delete(child); break; } } }); } - function clearElementAnimationState(element) { - var node = getDomNode(element); + function clearElementAnimationState(node) { node.removeAttribute(NG_ANIMATE_ATTR_NAME); - activeAnimationsLookup.remove(node); - } - - function isMatchingElement(nodeOrElmA, nodeOrElmB) { - return getDomNode(nodeOrElmA) === getDomNode(nodeOrElmB); + activeAnimationsLookup.delete(node); } /** @@ -2743,54 +2752,54 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { * 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); + function areAnimationsAllowed(node, parentNode, event) { + var bodyNode = $document[0].body; + var rootNode = getDomNode($rootElement); + + var bodyNodeDetected = (node === bodyNode) || node.nodeName === 'HTML'; + var rootNodeDetected = (node === rootNode); var parentAnimationDetected = false; + var elementDisabled = disabledElementsLookup.get(node); var animateChildren; - var elementDisabled = disabledElementsLookup.get(getDomNode(element)); - var parentHost = jqLite.data(element[0], NG_ANIMATE_PIN_DATA); + var parentHost = jqLite.data(node, NG_ANIMATE_PIN_DATA); if (parentHost) { - parentElement = parentHost; + parentNode = getDomNode(parentHost); } - parentElement = getDomNode(parentElement); - - while (parentElement) { - if (!rootElementDetected) { + while (parentNode) { + if (!rootNodeDetected) { // 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); + rootNodeDetected = (parentNode === rootNode); } - if (parentElement.nodeType !== ELEMENT_NODE) { + if (parentNode.nodeType !== ELEMENT_NODE) { // no point in inspecting the #document element break; } - var details = activeAnimationsLookup.get(parentElement) || {}; + var details = activeAnimationsLookup.get(parentNode) || {}; // 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); + var parentNodeDisabled = disabledElementsLookup.get(parentNode); - if (parentElementDisabled === true && elementDisabled !== false) { + if (parentNodeDisabled === 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) { + } else if (parentNodeDisabled === false) { elementDisabled = false; } parentAnimationDetected = details.structural; } if (isUndefined(animateChildren) || animateChildren === true) { - var value = jqLite.data(parentElement, NG_ANIMATE_CHILDREN_DATA); + var value = jqLite.data(parentNode, NG_ANIMATE_CHILDREN_DATA); if (isDefined(value)) { animateChildren = value; } @@ -2799,52 +2808,53 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { // there is no need to continue traversing at this point if (parentAnimationDetected && animateChildren === false) break; - if (!bodyElementDetected) { + if (!bodyNodeDetected) { // 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); + bodyNodeDetected = (parentNode === bodyNode); } - if (bodyElementDetected && rootElementDetected) { + if (bodyNodeDetected && rootNodeDetected) { // 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 (!rootNodeDetected) { + // If `rootNode` is not detected, check if `parentNode` is pinned to another element + parentHost = jqLite.data(parentNode, NG_ANIMATE_PIN_DATA); if (parentHost) { // The pin target element becomes the next parent element - parentElement = getDomNode(parentHost); + parentNode = getDomNode(parentHost); continue; } } - parentElement = parentElement.parentNode; + parentNode = parentNode.parentNode; } var allowAnimation = (!parentAnimationDetected || animateChildren) && elementDisabled !== true; - return allowAnimation && rootElementDetected && bodyElementDetected; + return allowAnimation && rootNodeDetected && bodyNodeDetected; } - function markElementAnimationState(element, state, details) { + function markElementAnimationState(node, 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); + activeAnimationsLookup.set(node, newValue); } }]; }]; -var $$AnimationProvider = ['$animateProvider', function($animateProvider) { +/* exported $$AnimationProvider */ + +var $$AnimationProvider = ['$animateProvider', /** @this */ function($animateProvider) { var NG_ANIMATE_REF_ATTR = 'ng-animate-ref'; var drivers = this.drivers = []; @@ -2863,21 +2873,21 @@ var $$AnimationProvider = ['$animateProvider', function($animateProvider) { return element.data(RUNNER_STORAGE_KEY); } - this.$get = ['$$jqLite', '$rootScope', '$injector', '$$AnimateRunner', '$$HashMap', '$$rAFScheduler', - function($$jqLite, $rootScope, $injector, $$AnimateRunner, $$HashMap, $$rAFScheduler) { + this.$get = ['$$jqLite', '$rootScope', '$injector', '$$AnimateRunner', '$$Map', '$$rAFScheduler', + function($$jqLite, $rootScope, $injector, $$AnimateRunner, $$Map, $$rAFScheduler) { var animationQueue = []; var applyAnimationClasses = applyAnimationClassesFactory($$jqLite); function sortAnimations(animations) { var tree = { children: [] }; - var i, lookup = new $$HashMap(); + var i, lookup = new $$Map(); - // this is done first beforehand so that the hashmap + // this is done first beforehand so that the map // 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] = { + lookup.set(animation.domNode, animations[i] = { domNode: animation.domNode, fn: animation.fn, children: [] @@ -2896,7 +2906,7 @@ var $$AnimationProvider = ['$animateProvider', function($animateProvider) { var elementNode = entry.domNode; var parentNode = elementNode.parentNode; - lookup.put(elementNode, entry); + lookup.set(elementNode, entry); var parentEntry; while (parentNode) { @@ -3232,7 +3242,7 @@ var $$AnimationProvider = ['$animateProvider', function($animateProvider) { } } - function close(rejected) { // jshint ignore:line + function close(rejected) { element.off('$destroy', handleDestroyedElement); removeRunner(element); @@ -3403,7 +3413,7 @@ var ngAnimateSwapDirective = ['$animate', '$rootScope', function($animate, $root * ## 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. + * and CSS code we can create an animation that will be picked up by Angular when an underlying directive performs an operation. * * The example below shows how an `enter` animation can be made possible on an element using `ng-if`: * @@ -3543,6 +3553,10 @@ var ngAnimateSwapDirective = ['$animate', '$rootScope', function($animate, $root * /* 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; + * + * /* if you are using animations instead of transitions you should configure as follows: + * animation-delay: 0.1s; + * animation-duration: 0s; */ * } * .my-animation.ng-enter.ng-enter-active { * /* standard transition styles */ @@ -3898,7 +3912,7 @@ var ngAnimateSwapDirective = ['$animate', '$rootScope', function($animate, $root deps="angular-animate.js;angular-route.js" animations="true"> <file name="index.html"> - <a href="#/">Home</a> + <a href="#!/">Home</a> <hr /> <div class="view-container"> <div ng-view class="view"></div> @@ -3918,22 +3932,23 @@ var ngAnimateSwapDirective = ['$animate', '$rootScope', function($animate, $root }]) .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" } + { 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) { + .controller('ProfileController', ['$rootScope', '$routeParams', + function ProfileController($rootScope, $routeParams) { var index = parseInt($routeParams.id, 10); var record = $rootScope.records[index - 1]; @@ -3945,7 +3960,7 @@ var ngAnimateSwapDirective = ['$animate', '$rootScope', function($animate, $root <h2>Welcome to the home page</h1> <p>Please click on an element</p> <a class="record" - ng-href="#/profile/{{ record.id }}" + ng-href="#!/profile/{{ record.id }}" ng-animate-ref="{{ record.id }}" ng-repeat="record in records"> {{ record.title }} @@ -4121,6 +4136,7 @@ angular.module('ngAnimate', [], function initAngularHelpers() { isFunction = angular.isFunction; isElement = angular.isElement; }) + .info({ angularVersion: '1.6.4' }) .directive('ngAnimateSwap', ngAnimateSwapDirective) .directive('ngAnimateChildren', $$AnimateChildrenDirective) @@ -4136,4 +4152,4 @@ angular.module('ngAnimate', [], function initAngularHelpers() { .provider('$$animateJsDriver', $$AnimateJsDriverProvider); -})(window, window.angular); +})(window, window.angular);
\ No newline at end of file diff --git a/static/js/angular-cookies.js b/static/js/angular-cookies.js index a03ff49f2f..505cad4bb7 100644 --- a/static/js/angular-cookies.js +++ b/static/js/angular-cookies.js @@ -1,6 +1,6 @@ /** - * @license AngularJS v1.5.8 - * (c) 2010-2016 Google, Inc. http://angularjs.org + * @license AngularJS v1.6.4 + * (c) 2010-2017 Google, Inc. http://angularjs.org * License: MIT */ (function(window, angular) {'use strict'; @@ -22,13 +22,14 @@ angular.module('ngCookies', ['ng']). + info({ angularVersion: '1.6.4' }). /** * @ngdoc provider * @name $cookiesProvider * @description * Use `$cookiesProvider` to change the default behavior of the {@link ngCookies.$cookies $cookies} service. * */ - provider('$cookies', [function $CookiesProvider() { + provider('$cookies', [/** @this */function $CookiesProvider() { /** * @ngdoc property * @name $cookiesProvider#defaults @@ -51,6 +52,16 @@ angular.module('ngCookies', ['ng']). * Note: By default, the address that appears in your `<base>` tag will be used as the path. * This is important so that cookies will be visible for all routes when html5mode is enabled. * + * @example + * + * ```js + * angular.module('cookiesProviderExample', ['ngCookies']) + * .config(['$cookiesProvider', function($cookiesProvider) { + * // Setting default options + * $cookiesProvider.defaults.domain = 'foo.com'; + * $cookiesProvider.defaults.secure = true; + * }]); + * ``` **/ var defaults = this.defaults = {}; @@ -184,6 +195,9 @@ angular.module('ngCookies'). * @ngdoc service * @name $cookieStore * @deprecated + * sinceVersion="v1.4.0" + * Please use the {@link ngCookies.$cookies `$cookies`} service instead. + * * @requires $cookies * * @description @@ -193,11 +207,6 @@ angular.module('ngCookies'). * * Requires the {@link ngCookies `ngCookies`} module to be installed. * - * <div class="alert alert-danger"> - * **Note:** The $cookieStore service is **deprecated**. - * Please use the {@link ngCookies.$cookies `$cookies`} service instead. - * </div> - * * @example * * ```js @@ -299,9 +308,9 @@ function $$CookieWriter($document, $log, $browser) { // - 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)!"); + $log.warn('Cookie \'' + name + + '\' possibly not set or overflowed because it was too large (' + + cookieLength + ' > 4096 bytes)!'); } return str; @@ -314,9 +323,9 @@ function $$CookieWriter($document, $log, $browser) { $$CookieWriter.$inject = ['$document', '$log', '$browser']; -angular.module('ngCookies').provider('$$cookieWriter', function $$CookieWriterProvider() { +angular.module('ngCookies').provider('$$cookieWriter', /** @this */ function $$CookieWriterProvider() { this.$get = $$CookieWriter; }); -})(window, window.angular); +})(window, window.angular);
\ No newline at end of file diff --git a/static/js/angular-resource.js b/static/js/angular-resource.js deleted file mode 100644 index e8bb301465..0000000000 --- a/static/js/angular-resource.js +++ /dev/null @@ -1,863 +0,0 @@ -/** - * @license AngularJS v1.5.8 - * (c) 2010-2016 Google, Inc. http://angularjs.org - * License: MIT - */ -(function(window, angular) {'use strict'; - -var $resourceMinErr = angular.$$minErr('$resource'); - -// Helper functions and regex to lookup a dotted path on an object -// stopping at undefined/null. The path must be composed of ASCII -// identifiers (just like $parse) -var MEMBER_NAME_REGEX = /^(\.[a-zA-Z_$@][0-9a-zA-Z_$@]*)+$/; - -function isValidDottedPath(path) { - return (path != null && path !== '' && path !== 'hasOwnProperty' && - MEMBER_NAME_REGEX.test('.' + path)); -} - -function lookupDottedPath(obj, path) { - if (!isValidDottedPath(path)) { - throw $resourceMinErr('badmember', 'Dotted member path "@{0}" is invalid.', path); - } - var keys = path.split('.'); - for (var i = 0, ii = keys.length; i < ii && angular.isDefined(obj); i++) { - var key = keys[i]; - obj = (obj !== null) ? obj[key] : undefined; - } - return obj; -} - -/** - * Create a shallow copy of an object and clear other fields from the destination - */ -function shallowClearAndCopy(src, dst) { - dst = dst || {}; - - angular.forEach(dst, function(value, key) { - delete dst[key]; - }); - - for (var key in src) { - if (src.hasOwnProperty(key) && !(key.charAt(0) === '$' && key.charAt(1) === '$')) { - dst[key] = src[key]; - } - } - - return dst; -} - -/** - * @ngdoc module - * @name ngResource - * @description - * - * # ngResource - * - * The `ngResource` module provides interaction support with RESTful services - * via the $resource service. - * - * - * <div doc-module-components="ngResource"></div> - * - * See {@link ngResource.$resourceProvider} and {@link ngResource.$resource} for usage. - */ - -/** - * @ngdoc provider - * @name $resourceProvider - * - * @description - * - * Use `$resourceProvider` to change the default behavior of the {@link ngResource.$resource} - * service. - * - * ## Dependencies - * Requires the {@link ngResource } module to be installed. - * - */ - -/** - * @ngdoc service - * @name $resource - * @requires $http - * @requires ng.$log - * @requires $q - * @requires ng.$timeout - * - * @description - * A factory which creates a resource object that lets you interact with - * [RESTful](http://en.wikipedia.org/wiki/Representational_State_Transfer) server-side data sources. - * - * The returned resource object has action methods which provide high-level behaviors without - * the need to interact with the low level {@link ng.$http $http} service. - * - * Requires the {@link ngResource `ngResource`} module to be installed. - * - * By default, trailing slashes will be stripped from the calculated URLs, - * which can pose problems with server backends that do not expect that - * behavior. This can be disabled by configuring the `$resourceProvider` like - * this: - * - * ```js - app.config(['$resourceProvider', function($resourceProvider) { - // Don't strip trailing slashes from calculated URLs - $resourceProvider.defaults.stripTrailingSlashes = false; - }]); - * ``` - * - * @param {string} url A parameterized URL template with parameters prefixed by `:` as in - * `/user/:username`. If you are using a URL with a port number (e.g. - * `http://example.com:8080/api`), it will be respected. - * - * If you are using a url with a suffix, just add the suffix, like this: - * `$resource('http://example.com/resource.json')` or `$resource('http://example.com/:id.json')` - * or even `$resource('http://example.com/resource/:resource_id.:format')` - * If the parameter before the suffix is empty, :resource_id in this case, then the `/.` will be - * collapsed down to a single `.`. If you need this sequence to appear and not collapse then you - * can escape it with `/\.`. - * - * @param {Object=} paramDefaults Default values for `url` parameters. These can be overridden in - * `actions` methods. If a parameter value is a function, it will be called every time - * a param value needs to be obtained for a request (unless the param was overridden). The function - * will be passed the current data value as an argument. - * - * Each key value in the parameter object is first bound to url template if present and then any - * excess keys are appended to the url search query after the `?`. - * - * Given a template `/path/:verb` and parameter `{verb:'greet', salutation:'Hello'}` results in - * URL `/path/greet?salutation=Hello`. - * - * If the parameter value is prefixed with `@`, then the value for that parameter will be - * extracted from the corresponding property on the `data` object (provided when calling a - * "non-GET" action method). - * For example, if the `defaultParam` object is `{someParam: '@someProp'}` then the value of - * `someParam` will be `data.someProp`. - * Note that the parameter will be ignored, when calling a "GET" action method (i.e. an action - * method that does not accept a request body) - * - * @param {Object.<Object>=} actions Hash with declaration of custom actions that should extend - * the default set of resource actions. The declaration should be created in the format of {@link - * ng.$http#usage $http.config}: - * - * {action1: {method:?, params:?, isArray:?, headers:?, ...}, - * action2: {method:?, params:?, isArray:?, headers:?, ...}, - * ...} - * - * Where: - * - * - **`action`** – {string} – The name of action. This name becomes the name of the method on - * your resource object. - * - **`method`** – {string} – Case insensitive HTTP method (e.g. `GET`, `POST`, `PUT`, - * `DELETE`, `JSONP`, etc). - * - **`params`** – {Object=} – Optional set of pre-bound parameters for this action. If any of - * the parameter value is a function, it will be called every time when a param value needs to - * be obtained for a request (unless the param was overridden). The function will be passed the - * current data value as an argument. - * - **`url`** – {string} – action specific `url` override. The url templating is supported just - * like for the resource-level urls. - * - **`isArray`** – {boolean=} – If true then the returned object for this action is an array, - * see `returns` section. - * - **`transformRequest`** – - * `{function(data, headersGetter)|Array.<function(data, headersGetter)>}` – - * 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. - * By default, transformRequest will contain one function that checks if the request data is - * an object and serializes to using `angular.toJson`. To prevent this behavior, set - * `transformRequest` to an empty array: `transformRequest: []` - * - **`transformResponse`** – - * `{function(data, headersGetter)|Array.<function(data, headersGetter)>}` – - * transform function or an array of such functions. The transform function takes the http - * response body and headers and returns its transformed (typically deserialized) version. - * By default, transformResponse will contain one function that checks if the response looks - * like a JSON string and deserializes it using `angular.fromJson`. To prevent this behavior, - * set `transformResponse` to an empty array: `transformResponse: []` - * - **`cache`** – `{boolean|Cache}` – If true, a default $http cache will be used to cache the - * GET request, otherwise if a cache instance built with - * {@link ng.$cacheFactory $cacheFactory}, this cache will be used for - * caching. - * - **`timeout`** – `{number}` – timeout in milliseconds.<br /> - * **Note:** In contrast to {@link ng.$http#usage $http.config}, {@link ng.$q promises} are - * **not** supported in $resource, because the same value would be used for multiple requests. - * If you are looking for a way to cancel requests, you should use the `cancellable` option. - * - **`cancellable`** – `{boolean}` – if set to true, the request made by a "non-instance" call - * will be cancelled (if not already completed) by calling `$cancelRequest()` on the call's - * return value. Calling `$cancelRequest()` for a non-cancellable or an already - * completed/cancelled request will have no effect.<br /> - * - **`withCredentials`** - `{boolean}` - whether to set the `withCredentials` flag on the - * XHR object. See - * [requests with credentials](https://developer.mozilla.org/en/http_access_control#section_5) - * for more information. - * - **`responseType`** - `{string}` - see - * [requestType](https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#responseType). - * - **`interceptor`** - `{Object=}` - The interceptor object has two optional methods - - * `response` and `responseError`. Both `response` and `responseError` interceptors get called - * with `http response` object. See {@link ng.$http $http interceptors}. - * - * @param {Object} options Hash with custom settings that should extend the - * default `$resourceProvider` behavior. The supported options are: - * - * - **`stripTrailingSlashes`** – {boolean} – If true then the trailing - * slashes from any calculated URL will be stripped. (Defaults to true.) - * - **`cancellable`** – {boolean} – If true, the request made by a "non-instance" call will be - * cancelled (if not already completed) by calling `$cancelRequest()` on the call's return value. - * This can be overwritten per action. (Defaults to false.) - * - * @returns {Object} A resource "class" object with methods for the default set of resource actions - * optionally extended with custom `actions`. The default set contains these actions: - * ```js - * { 'get': {method:'GET'}, - * 'save': {method:'POST'}, - * 'query': {method:'GET', isArray:true}, - * 'remove': {method:'DELETE'}, - * 'delete': {method:'DELETE'} }; - * ``` - * - * Calling these methods invoke an {@link ng.$http} with the specified http method, - * destination and parameters. When the data is returned from the server then the object is an - * instance of the resource class. The actions `save`, `remove` and `delete` are available on it - * as methods with the `$` prefix. This allows you to easily perform CRUD operations (create, - * read, update, delete) on server-side data like this: - * ```js - * var User = $resource('/user/:userId', {userId:'@id'}); - * var user = User.get({userId:123}, function() { - * user.abc = true; - * user.$save(); - * }); - * ``` - * - * It is important to realize that invoking a $resource object method immediately returns an - * empty reference (object or array depending on `isArray`). Once the data is returned from the - * server the existing reference is populated with the actual data. This is a useful trick since - * usually the resource is assigned to a model which is then rendered by the view. Having an empty - * object results in no rendering, once the data arrives from the server then the object is - * populated with the data and the view automatically re-renders itself showing the new data. This - * means that in most cases one never has to write a callback function for the action methods. - * - * The action methods on the class object or instance object can be invoked with the following - * parameters: - * - * - HTTP GET "class" actions: `Resource.action([parameters], [success], [error])` - * - non-GET "class" actions: `Resource.action([parameters], postData, [success], [error])` - * - non-GET instance actions: `instance.$action([parameters], [success], [error])` - * - * - * Success callback is called with (value, responseHeaders) arguments, where the value is - * the populated resource instance or collection object. The error callback is called - * with (httpResponse) argument. - * - * Class actions return empty instance (with additional properties below). - * Instance actions return promise of the action. - * - * The Resource instances and collections have these additional properties: - * - * - `$promise`: the {@link ng.$q promise} of the original server interaction that created this - * instance or collection. - * - * On success, the promise is resolved with the same resource instance or collection object, - * updated with data from server. This makes it easy to use in - * {@link ngRoute.$routeProvider resolve section of $routeProvider.when()} to defer view - * rendering until the resource(s) are loaded. - * - * On failure, the promise is rejected with the {@link ng.$http http response} object, without - * the `resource` property. - * - * If an interceptor object was provided, the promise will instead be resolved with the value - * returned by the interceptor. - * - * - `$resolved`: `true` after first server interaction is completed (either with success or - * rejection), `false` before that. Knowing if the Resource has been resolved is useful in - * data-binding. - * - * The Resource instances and collections have these additional methods: - * - * - `$cancelRequest`: If there is a cancellable, pending request related to the instance or - * collection, calling this method will abort the request. - * - * The Resource instances have these additional methods: - * - * - `toJSON`: It returns a simple object without any of the extra properties added as part of - * the Resource API. This object can be serialized through {@link angular.toJson} safely - * without attaching Angular-specific fields. Notice that `JSON.stringify` (and - * `angular.toJson`) automatically use this method when serializing a Resource instance - * (see [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#toJSON()_behavior)). - * - * @example - * - * # Credit card resource - * - * ```js - // Define CreditCard class - var CreditCard = $resource('/user/:userId/card/:cardId', - {userId:123, cardId:'@id'}, { - charge: {method:'POST', params:{charge:true}} - }); - - // We can retrieve a collection from the server - var cards = CreditCard.query(function() { - // GET: /user/123/card - // server returns: [ {id:456, number:'1234', name:'Smith'} ]; - - var card = cards[0]; - // each item is an instance of CreditCard - expect(card instanceof CreditCard).toEqual(true); - card.name = "J. Smith"; - // non GET methods are mapped onto the instances - card.$save(); - // POST: /user/123/card/456 {id:456, number:'1234', name:'J. Smith'} - // server returns: {id:456, number:'1234', name: 'J. Smith'}; - - // our custom method is mapped as well. - card.$charge({amount:9.99}); - // POST: /user/123/card/456?amount=9.99&charge=true {id:456, number:'1234', name:'J. Smith'} - }); - - // we can create an instance as well - var newCard = new CreditCard({number:'0123'}); - newCard.name = "Mike Smith"; - newCard.$save(); - // POST: /user/123/card {number:'0123', name:'Mike Smith'} - // server returns: {id:789, number:'0123', name: 'Mike Smith'}; - expect(newCard.id).toEqual(789); - * ``` - * - * The object returned from this function execution is a resource "class" which has "static" method - * for each action in the definition. - * - * Calling these methods invoke `$http` on the `url` template with the given `method`, `params` and - * `headers`. - * - * @example - * - * # User resource - * - * When the data is returned from the server then the object is an instance of the resource type and - * all of the non-GET methods are available with `$` prefix. This allows you to easily support CRUD - * operations (create, read, update, delete) on server-side data. - - ```js - var User = $resource('/user/:userId', {userId:'@id'}); - User.get({userId:123}, function(user) { - user.abc = true; - user.$save(); - }); - ``` - * - * It's worth noting that the success callback for `get`, `query` and other methods gets passed - * in the response that came from the server as well as $http header getter function, so one - * could rewrite the above example and get access to http headers as: - * - ```js - var User = $resource('/user/:userId', {userId:'@id'}); - User.get({userId:123}, function(user, getResponseHeaders){ - user.abc = true; - user.$save(function(user, putResponseHeaders) { - //user => saved user object - //putResponseHeaders => $http header getter - }); - }); - ``` - * - * You can also access the raw `$http` promise via the `$promise` property on the object returned - * - ``` - var User = $resource('/user/:userId', {userId:'@id'}); - User.get({userId:123}) - .$promise.then(function(user) { - $scope.user = user; - }); - ``` - * - * @example - * - * # Creating a custom 'PUT' request - * - * In this example we create a custom method on our resource to make a PUT request - * ```js - * var app = angular.module('app', ['ngResource', 'ngRoute']); - * - * // Some APIs expect a PUT request in the format URL/object/ID - * // Here we are creating an 'update' method - * app.factory('Notes', ['$resource', function($resource) { - * return $resource('/notes/:id', null, - * { - * 'update': { method:'PUT' } - * }); - * }]); - * - * // In our controller we get the ID from the URL using ngRoute and $routeParams - * // We pass in $routeParams and our Notes factory along with $scope - * app.controller('NotesCtrl', ['$scope', '$routeParams', 'Notes', - function($scope, $routeParams, Notes) { - * // First get a note object from the factory - * var note = Notes.get({ id:$routeParams.id }); - * $id = note.id; - * - * // Now call update passing in the ID first then the object you are updating - * Notes.update({ id:$id }, note); - * - * // This will PUT /notes/ID with the note object in the request payload - * }]); - * ``` - * - * @example - * - * # Cancelling requests - * - * If an action's configuration specifies that it is cancellable, you can cancel the request related - * to an instance or collection (as long as it is a result of a "non-instance" call): - * - ```js - // ...defining the `Hotel` resource... - var Hotel = $resource('/api/hotel/:id', {id: '@id'}, { - // Let's make the `query()` method cancellable - query: {method: 'get', isArray: true, cancellable: true} - }); - - // ...somewhere in the PlanVacationController... - ... - this.onDestinationChanged = function onDestinationChanged(destination) { - // We don't care about any pending request for hotels - // in a different destination any more - this.availableHotels.$cancelRequest(); - - // Let's query for hotels in '<destination>' - // (calls: /api/hotel?location=<destination>) - this.availableHotels = Hotel.query({location: destination}); - }; - ``` - * - */ -angular.module('ngResource', ['ng']). - provider('$resource', function() { - var PROTOCOL_AND_DOMAIN_REGEX = /^https?:\/\/[^\/]*/; - var provider = this; - - /** - * @ngdoc property - * @name $resourceProvider#defaults - * @description - * Object containing default options used when creating `$resource` instances. - * - * The default values satisfy a wide range of usecases, but you may choose to overwrite any of - * them to further customize your instances. The available properties are: - * - * - **stripTrailingSlashes** – `{boolean}` – If true, then the trailing slashes from any - * calculated URL will be stripped.<br /> - * (Defaults to true.) - * - **cancellable** – `{boolean}` – If true, the request made by a "non-instance" call will be - * cancelled (if not already completed) by calling `$cancelRequest()` on the call's return - * value. For more details, see {@link ngResource.$resource}. This can be overwritten per - * resource class or action.<br /> - * (Defaults to false.) - * - **actions** - `{Object.<Object>}` - A hash with default actions declarations. Actions are - * high-level methods corresponding to RESTful actions/methods on resources. An action may - * specify what HTTP method to use, what URL to hit, if the return value will be a single - * object or a collection (array) of objects etc. For more details, see - * {@link ngResource.$resource}. The actions can also be enhanced or overwritten per resource - * class.<br /> - * The default actions are: - * ```js - * { - * get: {method: 'GET'}, - * save: {method: 'POST'}, - * query: {method: 'GET', isArray: true}, - * remove: {method: 'DELETE'}, - * delete: {method: 'DELETE'} - * } - * ``` - * - * #### Example - * - * For example, you can specify a new `update` action that uses the `PUT` HTTP verb: - * - * ```js - * angular. - * module('myApp'). - * config(['resourceProvider', function ($resourceProvider) { - * $resourceProvider.defaults.actions.update = { - * method: 'PUT' - * }; - * }); - * ``` - * - * Or you can even overwrite the whole `actions` list and specify your own: - * - * ```js - * angular. - * module('myApp'). - * config(['resourceProvider', function ($resourceProvider) { - * $resourceProvider.defaults.actions = { - * create: {method: 'POST'} - * get: {method: 'GET'}, - * getAll: {method: 'GET', isArray:true}, - * update: {method: 'PUT'}, - * delete: {method: 'DELETE'} - * }; - * }); - * ``` - * - */ - this.defaults = { - // Strip slashes by default - stripTrailingSlashes: true, - - // Make non-instance requests cancellable (via `$cancelRequest()`) - cancellable: false, - - // Default actions configuration - actions: { - 'get': {method: 'GET'}, - 'save': {method: 'POST'}, - 'query': {method: 'GET', isArray: true}, - 'remove': {method: 'DELETE'}, - 'delete': {method: 'DELETE'} - } - }; - - this.$get = ['$http', '$log', '$q', '$timeout', function($http, $log, $q, $timeout) { - - var noop = angular.noop, - forEach = angular.forEach, - extend = angular.extend, - copy = angular.copy, - isFunction = angular.isFunction; - - /** - * 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(/%20/g, (pctEncodeSpaces ? '%20' : '+')); - } - - function Route(template, defaults) { - this.template = template; - this.defaults = extend({}, provider.defaults, defaults); - this.urlParams = {}; - } - - Route.prototype = { - setUrlParams: function(config, params, actionUrl) { - var self = this, - url = actionUrl || self.template, - val, - encodedVal, - protocolAndDomain = ''; - - var urlParams = self.urlParams = {}; - forEach(url.split(/\W/), function(param) { - if (param === 'hasOwnProperty') { - throw $resourceMinErr('badname', "hasOwnProperty is not a valid parameter name."); - } - if (!(new RegExp("^\\d+$").test(param)) && param && - (new RegExp("(^|[^\\\\]):" + param + "(\\W|$)").test(url))) { - urlParams[param] = { - isQueryParamValue: (new RegExp("\\?.*=:" + param + "(?:\\W|$)")).test(url) - }; - } - }); - url = url.replace(/\\:/g, ':'); - url = url.replace(PROTOCOL_AND_DOMAIN_REGEX, function(match) { - protocolAndDomain = match; - return ''; - }); - - params = params || {}; - forEach(self.urlParams, function(paramInfo, urlParam) { - val = params.hasOwnProperty(urlParam) ? params[urlParam] : self.defaults[urlParam]; - if (angular.isDefined(val) && val !== null) { - if (paramInfo.isQueryParamValue) { - encodedVal = encodeUriQuery(val, true); - } else { - encodedVal = encodeUriSegment(val); - } - url = url.replace(new RegExp(":" + urlParam + "(\\W|$)", "g"), function(match, p1) { - return encodedVal + p1; - }); - } else { - url = url.replace(new RegExp("(\/?):" + urlParam + "(\\W|$)", "g"), function(match, - leadingSlashes, tail) { - if (tail.charAt(0) == '/') { - return tail; - } else { - return leadingSlashes + tail; - } - }); - } - }); - - // strip trailing slashes and set the url (unless this behavior is specifically disabled) - if (self.defaults.stripTrailingSlashes) { - url = url.replace(/\/+$/, '') || '/'; - } - - // then replace collapse `/.` if found in the last URL path segment before the query - // E.g. `http://url.com/id./format?q=x` becomes `http://url.com/id.format?q=x` - url = url.replace(/\/\.(?=\w+($|\?))/, '.'); - // replace escaped `/\.` with `/.` - config.url = protocolAndDomain + url.replace(/\/\\\./, '/.'); - - - // set params - delegate param encoding to $http - forEach(params, function(value, key) { - if (!self.urlParams[key]) { - config.params = config.params || {}; - config.params[key] = value; - } - }); - } - }; - - - function resourceFactory(url, paramDefaults, actions, options) { - var route = new Route(url, options); - - actions = extend({}, provider.defaults.actions, actions); - - function extractParams(data, actionParams) { - var ids = {}; - actionParams = extend({}, paramDefaults, actionParams); - forEach(actionParams, function(value, key) { - if (isFunction(value)) { value = value(data); } - ids[key] = value && value.charAt && value.charAt(0) == '@' ? - lookupDottedPath(data, value.substr(1)) : value; - }); - return ids; - } - - function defaultResponseInterceptor(response) { - return response.resource; - } - - function Resource(value) { - shallowClearAndCopy(value || {}, this); - } - - Resource.prototype.toJSON = function() { - var data = extend({}, this); - delete data.$promise; - delete data.$resolved; - return data; - }; - - forEach(actions, function(action, name) { - var hasBody = /^(POST|PUT|PATCH)$/i.test(action.method); - var numericTimeout = action.timeout; - var cancellable = angular.isDefined(action.cancellable) ? action.cancellable : - (options && angular.isDefined(options.cancellable)) ? options.cancellable : - provider.defaults.cancellable; - - if (numericTimeout && !angular.isNumber(numericTimeout)) { - $log.debug('ngResource:\n' + - ' Only numeric values are allowed as `timeout`.\n' + - ' Promises are not supported in $resource, because the same value would ' + - 'be used for multiple requests. If you are looking for a way to cancel ' + - 'requests, you should use the `cancellable` option.'); - delete action.timeout; - numericTimeout = null; - } - - Resource[name] = function(a1, a2, a3, a4) { - var params = {}, data, success, error; - - /* jshint -W086 */ /* (purposefully fall through case statements) */ - switch (arguments.length) { - case 4: - error = a4; - success = a3; - //fallthrough - case 3: - case 2: - if (isFunction(a2)) { - if (isFunction(a1)) { - success = a1; - error = a2; - break; - } - - success = a2; - error = a3; - //fallthrough - } else { - params = a1; - data = a2; - success = a3; - break; - } - case 1: - if (isFunction(a1)) success = a1; - else if (hasBody) data = a1; - else params = a1; - break; - case 0: break; - default: - throw $resourceMinErr('badargs', - "Expected up to 4 arguments [params, data, success, error], got {0} arguments", - arguments.length); - } - /* jshint +W086 */ /* (purposefully fall through case statements) */ - - var isInstanceCall = this instanceof Resource; - var value = isInstanceCall ? data : (action.isArray ? [] : new Resource(data)); - var httpConfig = {}; - var responseInterceptor = action.interceptor && action.interceptor.response || - defaultResponseInterceptor; - var responseErrorInterceptor = action.interceptor && action.interceptor.responseError || - undefined; - var timeoutDeferred; - var numericTimeoutPromise; - - forEach(action, function(value, key) { - switch (key) { - default: - httpConfig[key] = copy(value); - break; - case 'params': - case 'isArray': - case 'interceptor': - case 'cancellable': - break; - } - }); - - if (!isInstanceCall && cancellable) { - timeoutDeferred = $q.defer(); - httpConfig.timeout = timeoutDeferred.promise; - - if (numericTimeout) { - numericTimeoutPromise = $timeout(timeoutDeferred.resolve, numericTimeout); - } - } - - if (hasBody) httpConfig.data = data; - route.setUrlParams(httpConfig, - extend({}, extractParams(data, action.params || {}), params), - action.url); - - var promise = $http(httpConfig).then(function(response) { - var data = response.data; - - if (data) { - // Need to convert action.isArray to boolean in case it is undefined - // jshint -W018 - if (angular.isArray(data) !== (!!action.isArray)) { - throw $resourceMinErr('badcfg', - 'Error in resource configuration for action `{0}`. Expected response to ' + - 'contain an {1} but got an {2} (Request: {3} {4})', name, action.isArray ? 'array' : 'object', - angular.isArray(data) ? 'array' : 'object', httpConfig.method, httpConfig.url); - } - // jshint +W018 - if (action.isArray) { - value.length = 0; - forEach(data, function(item) { - if (typeof item === "object") { - value.push(new Resource(item)); - } else { - // Valid JSON values may be string literals, and these should not be converted - // into objects. These items will not have access to the Resource prototype - // methods, but unfortunately there - value.push(item); - } - }); - } else { - var promise = value.$promise; // Save the promise - shallowClearAndCopy(data, value); - value.$promise = promise; // Restore the promise - } - } - response.resource = value; - - return response; - }, function(response) { - (error || noop)(response); - return $q.reject(response); - }); - - promise['finally'](function() { - value.$resolved = true; - if (!isInstanceCall && cancellable) { - value.$cancelRequest = angular.noop; - $timeout.cancel(numericTimeoutPromise); - timeoutDeferred = numericTimeoutPromise = httpConfig.timeout = null; - } - }); - - promise = promise.then( - function(response) { - var value = responseInterceptor(response); - (success || noop)(value, response.headers); - return value; - }, - responseErrorInterceptor); - - if (!isInstanceCall) { - // we are creating instance / collection - // - set the initial promise - // - return the instance / collection - value.$promise = promise; - value.$resolved = false; - if (cancellable) value.$cancelRequest = timeoutDeferred.resolve; - - return value; - } - - // instance call - return promise; - }; - - - Resource.prototype['$' + name] = function(params, success, error) { - if (isFunction(params)) { - error = success; success = params; params = {}; - } - var result = Resource[name].call(this, params, this, success, error); - return result.$promise || result; - }; - }); - - Resource.bind = function(additionalParamDefaults) { - return resourceFactory(url, extend({}, paramDefaults, additionalParamDefaults), actions); - }; - - return Resource; - } - - return resourceFactory; - }]; - }); - - -})(window, window.angular); diff --git a/static/js/angular-sanitize.js b/static/js/angular-sanitize.js index a283e43bde..30c3fe5fcc 100644 --- a/static/js/angular-sanitize.js +++ b/static/js/angular-sanitize.js @@ -1,6 +1,6 @@ /** - * @license AngularJS v1.5.8 - * (c) 2010-2016 Google, Inc. http://angularjs.org + * @license AngularJS v1.6.4 + * (c) 2010-2017 Google, Inc. http://angularjs.org * License: MIT */ (function(window, angular) {'use strict'; @@ -23,6 +23,7 @@ var forEach; var isDefined; var lowercase; var noop; +var nodeContains; var htmlParser; var htmlSanitizeWriter; @@ -63,7 +64,7 @@ var htmlSanitizeWriter; * @returns {string} Sanitized HTML. * * @example - <example module="sanitizeExample" deps="angular-sanitize.js"> + <example module="sanitizeExample" deps="angular-sanitize.js" name="sanitize-service"> <file name="index.html"> <script> angular.module('sanitizeExample', ['ngSanitize']) @@ -112,19 +113,19 @@ var htmlSanitizeWriter; </file> <file name="protractor.js" type="protractor"> it('should sanitize the html snippet by default', function() { - expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()). + expect(element(by.css('#bind-html-with-sanitize div')).getAttribute('innerHTML')). toBe('<p>an html\n<em>click here</em>\nsnippet</p>'); }); it('should inline raw snippet if bound to a trusted value', function() { - expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()). + expect(element(by.css('#bind-html-with-trust div')).getAttribute('innerHTML')). toBe("<p style=\"color:blue\">an html\n" + "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" + "snippet</p>"); }); it('should escape snippet without any filter', function() { - expect(element(by.css('#bind-default div')).getInnerHtml()). + expect(element(by.css('#bind-default div')).getAttribute('innerHTML')). toBe("<p style=\"color:blue\">an html\n" + "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" + "snippet</p>"); @@ -133,11 +134,11 @@ var htmlSanitizeWriter; it('should update', function() { element(by.model('snippet')).clear(); element(by.model('snippet')).sendKeys('new <b onclick="alert(1)">text</b>'); - expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()). + expect(element(by.css('#bind-html-with-sanitize div')).getAttribute('innerHTML')). toBe('new <b>text</b>'); - expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()).toBe( + expect(element(by.css('#bind-html-with-trust div')).getAttribute('innerHTML')).toBe( 'new <b onclick="alert(1)">text</b>'); - expect(element(by.css('#bind-default div')).getInnerHtml()).toBe( + expect(element(by.css('#bind-default div')).getAttribute('innerHTML')).toBe( "new <b onclick=\"alert(1)\">text</b>"); }); </file> @@ -148,6 +149,7 @@ var htmlSanitizeWriter; /** * @ngdoc provider * @name $sanitizeProvider + * @this * * @description * Creates and configures {@link $sanitize} instance. @@ -222,10 +224,15 @@ function $SanitizeProvider() { htmlParser = htmlParserImpl; htmlSanitizeWriter = htmlSanitizeWriterImpl; + nodeContains = window.Node.prototype.contains || /** @this */ function(arg) { + // eslint-disable-next-line no-bitwise + return !!(this.compareDocumentPosition(arg) & 16); + }; + // 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; + NON_ALPHANUMERIC_REGEXP = /([^#-~ |!])/g; // Good source of info about elements and attributes @@ -234,36 +241,36 @@ function $SanitizeProvider() { // Safe Void Elements - HTML5 // http://dev.w3.org/html5/spec/Overview.html#void-elements - var voidElements = toMap("area,br,col,hr,img,wbr"); + var voidElements = toMap('area,br,col,hr,img,wbr'); // Elements that you can, intentionally, leave open (and which close themselves) // http://dev.w3.org/html5/spec/Overview.html#optional-tags - var optionalEndTagBlockElements = toMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"), - optionalEndTagInlineElements = toMap("rp,rt"), + 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")); + 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")); + 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"); + 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 blockedElements = toMap('script,style'); var validElements = extend({}, voidElements, @@ -272,7 +279,7 @@ function $SanitizeProvider() { optionalEndTagElements); //Attributes that have href and hence need to be sanitized - var uriAttrs = toMap("background,cite,href,longdesc,src,xlink:href"); + 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,' + @@ -315,9 +322,9 @@ function $SanitizeProvider() { (function(window) { var doc; if (window.document && window.document.implementation) { - doc = window.document.implementation.createHTMLDocument("inert"); + doc = window.document.implementation.createHTMLDocument('inert'); } else { - throw $sanitizeMinErr('noinert', "Can't create an inert html document"); + throw $sanitizeMinErr('noinert', 'Can\'t create an inert html document'); } var docElement = doc.documentElement || doc.getDocumentElement(); var bodyElements = docElement.getElementsByTagName('body'); @@ -357,7 +364,7 @@ function $SanitizeProvider() { var mXSSAttempts = 5; do { if (mXSSAttempts === 0) { - throw $sanitizeMinErr('uinput', "Failed to sanitize html because the input is unstable"); + throw $sanitizeMinErr('uinput', 'Failed to sanitize html because the input is unstable'); } mXSSAttempts--; @@ -382,16 +389,16 @@ function $SanitizeProvider() { var nextNode; if (!(nextNode = node.firstChild)) { - if (node.nodeType == 1) { + if (node.nodeType === 1) { handler.end(node.nodeName.toLowerCase()); } - nextNode = node.nextSibling; + nextNode = getNonDescendant('nextSibling', node); if (!nextNode) { while (nextNode == null) { - node = node.parentNode; + node = getNonDescendant('parentNode', node); if (node === inertBodyElement) break; - nextNode = node.nextSibling; - if (node.nodeType == 1) { + nextNode = getNonDescendant('nextSibling', node); + if (node.nodeType === 1) { handler.end(node.nodeName.toLowerCase()); } } @@ -400,7 +407,7 @@ function $SanitizeProvider() { node = nextNode; } - while (node = inertBodyElement.firstChild) { + while ((node = inertBodyElement.firstChild)) { inertBodyElement.removeChild(node); } } @@ -481,6 +488,7 @@ function $SanitizeProvider() { out(tag); out('>'); } + // eslint-disable-next-line eqeqeq if (tag == ignoreCurrentElement) { ignoreCurrentElement = false; } @@ -502,28 +510,36 @@ function $SanitizeProvider() { * @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--; + while (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); + var nextNode = node.firstChild; + if (nextNode) { + stripCustomNsAttrs(nextNode); + } + + node = getNonDescendant('nextSibling', node); } + } - nextNode = node.nextSibling; - if (nextNode) { - stripCustomNsAttrs(nextNode); + function getNonDescendant(propName, node) { + // An element is clobbered if its `propName` property points to one of its descendants + var nextNode = node[propName]; + if (nextNode && nodeContains.call(node, nextNode)) { + throw $sanitizeMinErr('elclob', 'Failed to sanitize html because the element is clobbered: {0}', node.outerHTML || node.outerText); } + return nextNode; } } @@ -536,7 +552,9 @@ function sanitizeText(chars) { // define ngSanitize module and register $sanitize service -angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider); +angular.module('ngSanitize', []) + .provider('$sanitize', $SanitizeProvider) + .info({ angularVersion: '1.6.4' }); /** * @ngdoc filter @@ -568,7 +586,7 @@ angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider); <span ng-bind-html="linky_expression | linky"></span> * * @example - <example module="linkyExample" deps="angular-sanitize.js"> + <example module="linkyExample" deps="angular-sanitize.js" name="linky-filter"> <file name="index.html"> <div ng-controller="ExampleController"> Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea> @@ -616,10 +634,10 @@ angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider); 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'+ + '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/'; }]); @@ -735,4 +753,4 @@ angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) { }]); -})(window, window.angular); +})(window, window.angular);
\ No newline at end of file diff --git a/static/js/angular-ui-router-uib-modal.js b/static/js/angular-ui-router-uib-modal.js new file mode 100644 index 0000000000..912dd31362 --- /dev/null +++ b/static/js/angular-ui-router-uib-modal.js @@ -0,0 +1,75 @@ +/** + * AngularJS module that adds support for ui-bootstrap modal states when using ui-router. + * + * @link https://github.com/nonplus/angular-ui-router-uib-modal + * + * @license angular-ui-router-uib-modal v0.0.11 + * (c) Copyright Stepan Riha <github@nonplus.net> + * License MIT + */ + +(function(angular) { + +"use strict"; +angular.module("ui.router.modal", ["ui.router"]) + .config(["$stateProvider", function ($stateProvider) { + var stateProviderState = $stateProvider.state; + $stateProvider["state"] = state; + function state(name, config) { + var stateName; + var options; + // check for $stateProvider.state({name: "state", ...}) usage + if (angular.isObject(name)) { + options = name; + stateName = options.name; + } + else { + options = config; + stateName = name; + } + if (options.modal) { + if (options.onEnter) { + throw new Error("Invalid modal state definition: The onEnter setting may not be specified."); + } + if (options.onExit) { + throw new Error("Invalid modal state definition: The onExit setting may not be specified."); + } + var openModal_1; + // Get modal.resolve keys from state.modal or state.resolve + var resolve_1 = (Array.isArray(options.modal) ? options.modal : []).concat(Object.keys(options.resolve || {})); + var inject_1 = ["$uibModal", "$state"]; + options.onEnter = function ($uibModal, $state) { + // Add resolved values to modal options + if (resolve_1.length) { + options.resolve = {}; + for (var i = 0; i < resolve_1.length; i++) { + options.resolve[resolve_1[i]] = injectedConstant(arguments[inject_1.length + i]); + } + } + var thisModal = openModal_1 = $uibModal.open(options); + openModal_1.result['finally'](function () { + if (thisModal === openModal_1) { + // Dialog was closed via $uibModalInstance.close/dismiss, go to our parent state + $state.go($state.get("^", stateName).name); + } + }); + }; + // Make sure that onEnter receives state.resolve configuration + options.onEnter["$inject"] = inject_1.concat(resolve_1); + options.onExit = function () { + if (openModal_1) { + // State has changed while dialog was open + openModal_1.close(); + openModal_1 = null; + } + }; + } + return stateProviderState.call($stateProvider, stateName, options); + } + }]); +function injectedConstant(val) { + return [function () { return val; }]; +} + + +})(window.angular);
\ No newline at end of file diff --git a/static/js/angular.js b/static/js/angular.js index 54f65588f4..29458ca9cd 100644 --- a/static/js/angular.js +++ b/static/js/angular.js @@ -1,6 +1,6 @@ /** - * @license AngularJS v1.5.8 - * (c) 2010-2016 Google, Inc. http://angularjs.org + * @license AngularJS v1.6.4 + * (c) 2010-2017 Google, Inc. http://angularjs.org * License: MIT */ (function(window) {'use strict'; @@ -38,130 +38,135 @@ function minErr(module, ErrorConstructor) { ErrorConstructor = ErrorConstructor || Error; return function() { - var SKIP_INDEXES = 2; - - var templateArgs = arguments, - code = templateArgs[0], + var code = arguments[0], + template = arguments[1], message = '[' + (module ? module + ':' : '') + code + '] ', - template = templateArgs[1], + templateArgs = sliceArgs(arguments, 2).map(function(arg) { + return toDebugString(arg, minErrConfig.objectMaxDepth); + }), paramPrefix, i; message += template.replace(/\{\d+\}/g, function(match) { - var index = +match.slice(1, -1), - shiftedIndex = index + SKIP_INDEXES; + var index = +match.slice(1, -1); - if (shiftedIndex < templateArgs.length) { - return toDebugString(templateArgs[shiftedIndex]); + if (index < templateArgs.length) { + return templateArgs[index]; } return match; }); - message += '\nhttp://errors.angularjs.org/1.5.8/' + + message += '\nhttp://errors.angularjs.org/1.6.4/' + (module ? module + '/' : '') + code; - for (i = SKIP_INDEXES, paramPrefix = '?'; i < templateArgs.length; i++, paramPrefix = '&') { - message += paramPrefix + 'p' + (i - SKIP_INDEXES) + '=' + - encodeURIComponent(toDebugString(templateArgs[i])); + for (i = 0, paramPrefix = '?'; i < templateArgs.length; i++, paramPrefix = '&') { + message += paramPrefix + 'p' + i + '=' + encodeURIComponent(templateArgs[i]); } return new ErrorConstructor(message); }; } -/* We need to tell jshint what variables are being exported */ -/* global angular: true, - msie: true, - jqLite: true, - jQuery: true, - slice: true, - splice: true, - push: true, - toString: true, - ngMinErr: true, - angularModule: true, - uid: true, - REGEX_STRING_REGEXP: true, - VALIDITY_STATE_PROPERTY: true, - - lowercase: true, - uppercase: true, - manualLowercase: true, - manualUppercase: true, - nodeName_: true, - isArrayLike: true, - forEach: true, - forEachSorted: true, - reverseParams: true, - nextUid: true, - setHashKey: true, - extend: true, - toInt: true, - inherit: true, - merge: true, - noop: true, - identity: true, - valueFn: true, - isUndefined: true, - isDefined: true, - isObject: true, - isBlankObject: true, - isString: true, - isNumber: true, - isDate: true, - isArray: true, - isFunction: true, - isRegExp: true, - isWindow: true, - isScope: true, - isFile: true, - isFormData: true, - isBlob: true, - isBoolean: true, - isPromiseLike: true, - trim: true, - escapeForRegexp: true, - isElement: true, - makeMap: true, - includes: true, - arrayRemove: true, - copy: true, - equals: true, - csp: true, - jq: true, - concat: true, - sliceArgs: true, - bind: true, - toJsonReplacer: true, - toJson: true, - fromJson: true, - convertTimezoneToLocal: true, - timezoneToOffset: true, - startingTag: true, - tryDecodeURIComponent: true, - parseKeyValue: true, - toKeyValue: true, - encodeUriSegment: true, - encodeUriQuery: true, - angularInit: true, - bootstrap: true, - getTestability: true, - snake_case: true, - bindJQuery: true, - assertArg: true, - assertArgFn: true, - assertNotHasOwnProperty: true, - getter: true, - getBlockNodes: true, - hasOwnProperty: true, - createMap: true, - - NODE_TYPE_ELEMENT: true, - NODE_TYPE_ATTRIBUTE: true, - NODE_TYPE_TEXT: true, - NODE_TYPE_COMMENT: true, - NODE_TYPE_DOCUMENT: true, - NODE_TYPE_DOCUMENT_FRAGMENT: true, +/* We need to tell ESLint what variables are being exported */ +/* exported + angular, + msie, + jqLite, + jQuery, + slice, + splice, + push, + toString, + minErrConfig, + errorHandlingConfig, + isValidObjectMaxDepth, + 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, + simpleCompare, + 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, + stringify, + + NODE_TYPE_ELEMENT, + NODE_TYPE_ATTRIBUTE, + NODE_TYPE_TEXT, + NODE_TYPE_COMMENT, + NODE_TYPE_DOCUMENT, + NODE_TYPE_DOCUMENT_FRAGMENT */ //////////////////////////////////// @@ -188,23 +193,101 @@ var REGEX_STRING_REGEXP = /^\/(.+)\/([a-z]*)$/; // 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 minErrConfig = { + objectMaxDepth: 5 +}; + + /** + * @ngdoc function + * @name angular.errorHandlingConfig + * @module ng + * @kind function + * + * @description + * Configure several aspects of error handling in AngularJS if used as a setter or return the + * current configuration if used as a getter. The following options are supported: + * + * - **objectMaxDepth**: The maximum depth to which objects are traversed when stringified for error messages. + * + * Omitted or undefined options will leave the corresponding configuration values unchanged. + * + * @param {Object=} config - The configuration object. May only contain the options that need to be + * updated. Supported keys: + * + * * `objectMaxDepth` **{Number}** - The max depth for stringifying objects. Setting to a + * non-positive or non-numeric value, removes the max depth limit. + * Default: 5 + */ +function errorHandlingConfig(config) { + if (isObject(config)) { + if (isDefined(config.objectMaxDepth)) { + minErrConfig.objectMaxDepth = isValidObjectMaxDepth(config.objectMaxDepth) ? config.objectMaxDepth : NaN; + } + } else { + return minErrConfig; + } +} + +/** + * @private + * @param {Number} maxDepth + * @return {boolean} + */ +function isValidObjectMaxDepth(maxDepth) { + return isNumber(maxDepth) && maxDepth > 0; +} + +/** + * @ngdoc function + * @name angular.lowercase + * @module ng + * @kind function + * + * @deprecated + * sinceVersion="1.5.0" + * removeVersion="1.7.0" + * Use [String.prototype.toLowerCase](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/toLowerCase) instead. + * + * @description Converts the specified string to lowercase. + * @param {string} string String to be converted to lowercase. + * @returns {string} Lowercased string. + */ var lowercase = function(string) {return isString(string) ? string.toLowerCase() : string;}; + +/** + * @ngdoc function + * @name angular.uppercase + * @module ng + * @kind function + * + * @deprecated + * sinceVersion="1.5.0" + * removeVersion="1.7.0" + * Use [String.prototype.toUpperCase](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/toUpperCase) instead. + * + * @description Converts the specified string to uppercase. + * @param {string} string String to be converted to uppercase. + * @returns {string} Uppercased string. + */ var uppercase = function(string) {return isString(string) ? string.toUpperCase() : string;}; var manualLowercase = function(s) { - /* jshint bitwise: false */ + /* 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) { - /* jshint bitwise: false */ + /* eslint-disable no-bitwise */ return isString(s) ? s.replace(/[a-z]/g, function(ch) {return String.fromCharCode(ch.charCodeAt(0) & ~32);}) : s; + /* eslint-enable */ }; @@ -233,6 +316,7 @@ var angularModule, uid = 0; +// Support: IE 9-11 only /** * documentMode is an IE-only property * http://msdn.microsoft.com/en-us/library/ie/cc196988(v=vs.85).aspx @@ -259,12 +343,12 @@ function isArrayLike(obj) { // 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; + 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'); + (length >= 0 && ((length - 1) in obj || obj instanceof Array) || typeof obj.item === 'function'); } @@ -308,9 +392,7 @@ function forEach(obj, iterator, context) { 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))) { + if (key !== 'prototype' && key !== 'length' && key !== 'name' && obj.hasOwnProperty(key)) { iterator.call(context, obj[key], key, obj); } } @@ -479,6 +561,11 @@ 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); @@ -662,7 +749,7 @@ function isDate(value) { * @kind function * * @description - * Determines if a reference is an `Array`. + * Determines if a reference is an `Array`. Alias of Array.isArray. * * @param {*} value Reference to check. * @returns {boolean} True if `value` is an `Array`. @@ -738,7 +825,7 @@ function isPromiseLike(obj) { } -var TYPED_ARRAY_REGEXP = /^\[object (?:Uint8|Uint8Clamped|Uint16|Uint32|Int8|Int16|Int32|Float32|Float64)Array\]$/; +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)); } @@ -756,8 +843,10 @@ var trim = function(value) { // 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(/([-()\[\]{}+?*.$\^|,:#<!\\])/g, '\\$1'). - replace(/\x08/g, '\\x08'); + return s + .replace(/([-()[\]{}+?*.$^|,:#<!\\])/g, '\\$1') + // eslint-disable-next-line no-control-regex + .replace(/\x08/g, '\\x08'); }; @@ -797,7 +886,7 @@ function nodeName_(element) { } function includes(array, obj) { - return Array.prototype.indexOf.call(array, obj) != -1; + return Array.prototype.indexOf.call(array, obj) !== -1; } function arrayRemove(array, value) { @@ -836,7 +925,7 @@ function arrayRemove(array, value) { * @returns {*} The copy or updated `destination`, if `destination` was specified. * * @example - <example module="copyExample"> + <example module="copyExample" name="angular-copy"> <file name="index.html"> <div ng-controller="ExampleController"> <form novalidate class="simple-form"> @@ -873,16 +962,17 @@ function arrayRemove(array, value) { </file> </example> */ -function copy(source, destination) { +function copy(source, destination, maxDepth) { var stackSource = []; var stackDest = []; + maxDepth = isValidObjectMaxDepth(maxDepth) ? maxDepth : NaN; if (destination) { if (isTypedArray(destination) || isArrayBuffer(destination)) { - throw ngMinErr('cpta', "Can't copy! TypedArray destination cannot be mutated."); + 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."); + throw ngMinErr('cpi', 'Can\'t copy! Source and destination are identical.'); } // Empty the destination object @@ -898,35 +988,39 @@ function copy(source, destination) { stackSource.push(source); stackDest.push(destination); - return copyRecurse(source, destination); + return copyRecurse(source, destination, maxDepth); } - return copyElement(source); + return copyElement(source, maxDepth); - function copyRecurse(source, destination) { + function copyRecurse(source, destination, maxDepth) { + maxDepth--; + if (maxDepth < 0) { + return '...'; + } var h = destination.$$hashKey; var key; if (isArray(source)) { for (var i = 0, ii = source.length; i < ii; i++) { - destination.push(copyElement(source[i])); + destination.push(copyElement(source[i], maxDepth)); } } else if (isBlankObject(source)) { // createMap() fast path --- Safe to avoid hasOwnProperty check because prototype chain is empty for (key in source) { - destination[key] = copyElement(source[key]); + destination[key] = copyElement(source[key], maxDepth); } } else if (source && typeof source.hasOwnProperty === 'function') { // Slow path, which must rely on hasOwnProperty for (key in source) { if (source.hasOwnProperty(key)) { - destination[key] = copyElement(source[key]); + destination[key] = copyElement(source[key], maxDepth); } } } 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]); + destination[key] = copyElement(source[key], maxDepth); } } } @@ -934,7 +1028,7 @@ function copy(source, destination) { return destination; } - function copyElement(source) { + function copyElement(source, maxDepth) { // Simple values if (!isObject(source)) { return source; @@ -948,7 +1042,7 @@ function copy(source, destination) { if (isWindow(source) || isScope(source)) { throw ngMinErr('cpws', - "Can't copy! Making copies of Window or Scope instances is not supported."); + 'Can\'t copy! Making copies of Window or Scope instances is not supported.'); } var needsRecurse = false; @@ -963,7 +1057,7 @@ function copy(source, destination) { stackDest.push(destination); return needsRecurse - ? copyRecurse(source, destination) + ? copyRecurse(source, destination, maxDepth) : destination; } @@ -981,10 +1075,13 @@ function copy(source, destination) { return new source.constructor(copyElement(source.buffer), source.byteOffset, source.length); case '[object ArrayBuffer]': - //Support: IE10 + // 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); @@ -996,7 +1093,7 @@ function copy(source, destination) { return new source.constructor(source.valueOf()); case '[object RegExp]': - var re = new RegExp(source.source, source.toString().match(/[^\/]*$/)[0]); + var re = new RegExp(source.source, source.toString().match(/[^/]*$/)[0]); re.lastIndex = source.lastIndex; return re; @@ -1011,6 +1108,10 @@ function copy(source, destination) { } +// eslint-disable-next-line no-self-compare +function simpleCompare(a, b) { return a === b || (a !== a && b !== b); } + + /** * @ngdoc function * @name angular.equals @@ -1067,7 +1168,6 @@ function copy(source, destination) { angular.module('equalsExample', []).controller('ExampleController', ['$scope', function($scope) { $scope.user1 = {}; $scope.user2 = {}; - $scope.result; $scope.compare = function() { $scope.result = angular.equals($scope.user1, $scope.user2); }; @@ -1078,12 +1178,13 @@ function copy(source, destination) { 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 (t1 === t2 && t1 === 'object') { if (isArray(o1)) { if (!isArray(o2)) return false; - if ((length = o1.length) == o2.length) { + if ((length = o1.length) === o2.length) { for (key = 0; key < length; key++) { if (!equals(o1[key], o2[key])) return false; } @@ -1091,10 +1192,10 @@ function equals(o1, o2) { } } else if (isDate(o1)) { if (!isDate(o2)) return false; - return equals(o1.getTime(), o2.getTime()); + return simpleCompare(o1.getTime(), o2.getTime()); } else if (isRegExp(o1)) { if (!isRegExp(o2)) return false; - return o1.toString() == o2.toString(); + return o1.toString() === o2.toString(); } else { if (isScope(o1) || isScope(o2) || isWindow(o1) || isWindow(o2) || isArray(o2) || isDate(o2) || isRegExp(o2)) return false; @@ -1142,9 +1243,8 @@ var csp = function() { function noUnsafeEval() { try { - /* jshint -W031, -W054 */ + // eslint-disable-next-line no-new, no-new-func new Function(''); - /* jshint +W031, +W054 */ return false; } catch (e) { return true; @@ -1196,7 +1296,8 @@ var jq = function() { var i, ii = ngAttrPrefixes.length, prefix, name; for (i = 0; i < ii; ++i) { prefix = ngAttrPrefixes[i]; - if (el = window.document.querySelector('[' + prefix.replace(':', '\\:') + 'jq]')) { + el = window.document.querySelector('[' + prefix.replace(':', '\\:') + 'jq]'); + if (el) { name = el.getAttribute(prefix + 'jq'); break; } @@ -1214,7 +1315,6 @@ function sliceArgs(args, startIndex) { } -/* jshint -W101 */ /** * @ngdoc function * @name angular.bind @@ -1232,7 +1332,6 @@ function sliceArgs(args, startIndex) { * @param {...*} args Optional arguments to be prebound to the `fn` function call. * @returns {function()} Function that wraps the `fn` with all the specified bindings. */ -/* jshint +W101 */ function bind(self, fn) { var curryArgs = arguments.length > 2 ? sliceArgs(arguments, 2) : []; if (isFunction(fn) && !(fn instanceof RegExp)) { @@ -1281,7 +1380,7 @@ function toJsonReplacer(key, value) { * Serializes input into a JSON-formatted string. Properties with leading $$ characters will be * stripped since angular uses this notation internally. * - * @param {Object|Array|Date|string|number} obj Input to be serialized into JSON. + * @param {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`. @@ -1337,10 +1436,11 @@ function fromJson(json) { var ALL_COLONS = /:/g; function timezoneToOffset(timezone, fallback) { + // Support: IE 9-11 only, Edge 13-14+ // 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 isNaN(requestedTimezoneOffset) ? fallback : requestedTimezoneOffset; + return isNumberNaN(requestedTimezoneOffset) ? fallback : requestedTimezoneOffset; } @@ -1368,13 +1468,13 @@ function startingTag(element) { // 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) {} + } catch (e) { /* empty */ } var elemHtml = jqLite('<div>').append(element).html(); try { return element[0].nodeType === NODE_TYPE_TEXT ? lowercase(elemHtml) : elemHtml. match(/^(<[^>]+>)/)[1]. - replace(/^<([\w\-]+)/, function(match, nodeName) {return '<' + lowercase(nodeName);}); + replace(/^<([\w-]+)/, function(match, nodeName) {return '<' + lowercase(nodeName);}); } catch (e) { return lowercase(elemHtml); } @@ -1407,7 +1507,7 @@ function tryDecodeURIComponent(value) { */ function parseKeyValue(/**string*/keyValue) { var obj = {}; - forEach((keyValue || "").split('&'), function(keyValue) { + forEach((keyValue || '').split('&'), function(keyValue) { var splitPoint, key, val; if (keyValue) { key = keyValue = keyValue.replace(/\+/g,'%20'); @@ -1472,7 +1572,7 @@ function encodeUriSegment(val) { * This method is intended for encoding *key* or *value* parts of query component. We need a custom * method because encodeURIComponent is too aggressive and encodes stuff that doesn't have to be * encoded per http://tools.ietf.org/html/rfc3986: - * query = *( pchar / "/" / "?" ) + * query = *( pchar / "/" / "?" ) * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" * pct-encoded = "%" HEXDIG HEXDIG @@ -1502,6 +1602,57 @@ function getNgAttribute(element, ngAttr) { return null; } +function allowAutoBootstrap(document) { + var script = document.currentScript; + + if (!script) { + // IE does not have `document.currentScript` + return true; + } + + // If the `currentScript` property has been clobbered just return false, since this indicates a probable attack + if (!(script instanceof window.HTMLScriptElement || script instanceof window.SVGScriptElement)) { + return false; + } + + var attributes = script.attributes; + var srcs = [attributes.getNamedItem('src'), attributes.getNamedItem('href'), attributes.getNamedItem('xlink:href')]; + + return srcs.every(function(src) { + if (!src) { + return true; + } + if (!src.value) { + return false; + } + + var link = document.createElement('a'); + link.href = src.value; + + if (document.location.origin === link.origin) { + // Same-origin resources are always allowed, even for non-whitelisted schemes. + return true; + } + // Disabled bootstrapping unless angular.js was loaded from a known scheme used on the web. + // This is to prevent angular.js bundled with browser extensions from being used to bypass the + // content security policy in web pages and other browser extensions. + switch (link.protocol) { + case 'http:': + case 'https:': + case 'ftp:': + case 'blob:': + case 'file:': + case 'data:': + return true; + default: + return false; + } + }); +} + +// Cached as it has to run during loading so that document.currentScript is available. +var isAutoBootstrapAllowed = allowAutoBootstrap(window.document); + /** * @ngdoc directive * @name ngApp @@ -1545,7 +1696,7 @@ function getNgAttribute(element, ngAttr) { * * `ngApp` is the easiest, and most common way to bootstrap an application. * - <example module="ngAppDemo"> + <example module="ngAppDemo" name="ng-app"> <file name="index.html"> <div ng-controller="ngAppDemoController"> I can add: {{a}} + {{b}} = {{ a+b }} @@ -1561,7 +1712,7 @@ function getNgAttribute(element, ngAttr) { * * Using `ngStrictDi`, you would see something like this: * - <example ng-app-included="true"> + <example ng-app-included="true" name="strict-di"> <file name="index.html"> <div ng-app="ngAppStrictDemo" ng-strict-di> <div ng-controller="GoodController1"> @@ -1610,7 +1761,7 @@ function getNgAttribute(element, ngAttr) { }]) .controller('GoodController2', GoodController2); function GoodController2($scope) { - $scope.name = "World"; + $scope.name = 'World'; } GoodController2.$inject = ['$scope']; </file> @@ -1660,7 +1811,12 @@ function angularInit(element, bootstrap) { } }); if (appElement) { - config.strictDi = getNgAttribute(appElement, "strict-di") !== null; + if (!isAutoBootstrapAllowed) { + window.console.error('Angular: disabling automatic bootstrap. <script> protocol indicates ' + + 'an extension, document.location.href does not match.'); + return; + } + config.strictDi = getNgAttribute(appElement, 'strict-di') !== null; bootstrap(appElement, module ? [module] : [], config); } } @@ -1738,7 +1894,7 @@ function bootstrap(element, modules, config) { // Encode angle brackets to prevent input from being sanitized to empty string #8683. throw ngMinErr( 'btstrpd', - "App already bootstrapped with this element '{0}'", + 'App already bootstrapped with this element \'{0}\'', tag.replace(/</,'<').replace(/>/,'>')); } @@ -1855,7 +2011,7 @@ function bindJQuery() { extend(jQuery.fn, { scope: JQLitePrototype.scope, isolateScope: JQLitePrototype.isolateScope, - controller: JQLitePrototype.controller, + controller: /** @type {?} */ (JQLitePrototype).controller, injector: JQLitePrototype.injector, inheritedData: JQLitePrototype.inheritedData }); @@ -1867,7 +2023,7 @@ function bindJQuery() { jQuery.cleanData = function(elems) { var events; for (var i = 0, elem; (elem = elems[i]) != null; i++) { - events = jQuery._data(elem, "events"); + events = jQuery._data(elem, 'events'); if (events && events.$destroy) { jQuery(elem).triggerHandler('$destroy'); } @@ -1889,7 +2045,7 @@ function bindJQuery() { */ function assertArg(arg, name, reason) { if (!arg) { - throw ngMinErr('areq', "Argument '{0}' is {1}", (name || '?'), (reason || "required")); + throw ngMinErr('areq', 'Argument \'{0}\' is {1}', (name || '?'), (reason || 'required')); } return arg; } @@ -1911,7 +2067,7 @@ function assertArgFn(arg, name, acceptArrayAnnotation) { */ function assertNotHasOwnProperty(name, context) { if (name === 'hasOwnProperty') { - throw ngMinErr('badname', "hasOwnProperty is not a valid {0} name", context); + throw ngMinErr('badname', 'hasOwnProperty is not a valid {0} name', context); } } @@ -1981,6 +2137,27 @@ function createMap() { return Object.create(null); } +function stringify(value) { + if (value == null) { // null || undefined + return ''; + } + switch (typeof value) { + case 'string': + break; + case 'number': + value = '' + value; + break; + default: + if (hasCustomToString(value) && !isArray(value) && !isDate(value)) { + value = value.toString(); + } else { + value = toJson(value); + } + } + + return value; +} + var NODE_TYPE_ELEMENT = 1; var NODE_TYPE_ATTRIBUTE = 2; var NODE_TYPE_TEXT = 3; @@ -2067,6 +2244,9 @@ function setupModuleLoader(window) { * @returns {angular.Module} new module with the {@link angular.Module} api. */ return function module(name, requires, configFn) { + + var info = {}; + var assertNotHasOwnProperty = function(name, context) { if (name === 'hasOwnProperty') { throw ngMinErr('badname', 'hasOwnProperty is not a valid {0} name', context); @@ -2079,9 +2259,9 @@ function setupModuleLoader(window) { } return ensure(modules, name, function() { if (!requires) { - throw $injectorMinErr('nomod', "Module '{0}' is not available! You either misspelled " + - "the module name or forgot to load it. If registering a module ensure that you " + - "specify the dependencies as the second argument.", name); + throw $injectorMinErr('nomod', 'Module \'{0}\' is not available! You either misspelled ' + + 'the module name or forgot to load it. If registering a module ensure that you ' + + 'specify the dependencies as the second argument.', name); } /** @type {!Array.<Array.<*>>} */ @@ -2103,6 +2283,45 @@ function setupModuleLoader(window) { _runBlocks: runBlocks, /** + * @ngdoc method + * @name angular.Module#info + * @module ng + * + * @param {Object=} info Information about the module + * @returns {Object|Module} The current info object for this module if called as a getter, + * or `this` if called as a setter. + * + * @description + * Read and write custom information about this module. + * For example you could put the version of the module in here. + * + * ```js + * angular.module('myModule', []).info({ version: '1.0.0' }); + * ``` + * + * The version could then be read back out by accessing the module elsewhere: + * + * ``` + * var version = angular.module('myModule').info().version; + * ``` + * + * You can also retrieve this information during runtime via the + * {@link $injector#modules `$injector.modules`} property: + * + * ```js + * var version = $injector.modules['myModule'].info().version; + * ``` + */ + info: function(value) { + if (isDefined(value)) { + if (!isObject(value)) throw ngMinErr('aobj', 'Argument \'{0}\' must be an object', 'value'); + info = value; + return this; + } + return info; + }, + + /** * @ngdoc property * @name angular.Module#requires * @module ng @@ -2191,7 +2410,7 @@ function setupModuleLoader(window) { * @description * See {@link auto.$provide#decorator $provide.decorator()}. */ - decorator: invokeLaterAndSetModuleName('$provide', 'decorator'), + decorator: invokeLaterAndSetModuleName('$provide', 'decorator', configBlocks), /** * @ngdoc method @@ -2337,10 +2556,11 @@ function setupModuleLoader(window) { * @param {string} method * @returns {angular.Module} */ - function invokeLaterAndSetModuleName(provider, method) { + function invokeLaterAndSetModuleName(provider, method, queue) { + if (!queue) queue = invokeQueue; return function(recipeName, factoryFunction) { if (factoryFunction && isFunction(factoryFunction)) factoryFunction.$$moduleName = name; - invokeQueue.push([provider, method, arguments]); + queue.push([provider, method, arguments]); return moduleInstance; }; } @@ -2379,9 +2599,15 @@ function shallowCopy(src, dst) { /* global toDebugString: true */ -function serializeObject(obj) { +function serializeObject(obj, maxDepth) { var seen = []; + // There is no direct way to stringify object until reaching a specific depth + // and a very deep object can cause a performance issue, so we copy the object + // based on this specific depth and then stringify it. + if (isValidObjectMaxDepth(maxDepth)) { + obj = copy(obj, null, maxDepth); + } return JSON.stringify(obj, function(key, val) { val = toJsonReplacer(key, val); if (isObject(val)) { @@ -2394,13 +2620,13 @@ function serializeObject(obj) { }); } -function toDebugString(obj) { +function toDebugString(obj, maxDepth) { if (typeof obj === 'function') { return obj.toString().replace(/ \{[\s\S]*$/, ''); } else if (isUndefined(obj)) { return 'undefined'; } else if (typeof obj !== 'string') { - return serializeObject(obj); + return serializeObject(obj, maxDepth); } return obj; } @@ -2416,7 +2642,6 @@ function toDebugString(obj) { formDirective, scriptDirective, selectDirective, - styleDirective, optionDirective, ngBindDirective, ngBindHtmlDirective, @@ -2470,12 +2695,12 @@ function toDebugString(obj) { $ControllerProvider, $DateProvider, $DocumentProvider, + $$IsDocumentHiddenProvider, $ExceptionHandlerProvider, $FilterProvider, $$ForceReflowProvider, $InterpolateProvider, $IntervalProvider, - $$HashMapProvider, $HttpProvider, $HttpParamSerializerProvider, $HttpParamSerializerJQLikeProvider, @@ -2484,6 +2709,7 @@ function toDebugString(obj) { $jsonpCallbacksProvider, $LocationProvider, $LogProvider, + $$MapProvider, $ParseProvider, $RootScopeProvider, $QProvider, @@ -2519,16 +2745,19 @@ function toDebugString(obj) { * - `codeName` – `{string}` – Code name of the release, such as "jiggling-armfat". */ var version = { - full: '1.5.8', // all of these placeholder strings will be replaced by grunt's - major: 1, // package task - minor: 5, - dot: 8, - codeName: 'arbitrary-fallbacks' + // These placeholder strings will be replaced by grunt's `build` task. + // They need to be double- or single-quoted. + full: '1.6.4', + major: 1, + minor: 6, + dot: 4, + codeName: 'phenomenal-footnote' }; function publishExternalAPI(angular) { extend(angular, { + 'errorHandlingConfig': errorHandlingConfig, 'bootstrap': bootstrap, 'copy': copy, 'extend': extend, @@ -2556,9 +2785,12 @@ function publishExternalAPI(angular) { 'uppercase': uppercase, 'callbacks': {$$counter: 0}, 'getTestability': getTestability, + 'reloadWithDebugInfo': reloadWithDebugInfo, '$$minErr': minErr, '$$csp': csp, - 'reloadWithDebugInfo': reloadWithDebugInfo + '$$encodeUriSegment': encodeUriSegment, + '$$encodeUriQuery': encodeUriQuery, + '$$stringify': stringify }); angularModule = setupModuleLoader(window); @@ -2577,7 +2809,6 @@ function publishExternalAPI(angular) { form: formDirective, script: scriptDirective, select: selectDirective, - style: styleDirective, option: optionDirective, ngBind: ngBindDirective, ngBindHtml: ngBindHtmlDirective, @@ -2633,6 +2864,7 @@ function publishExternalAPI(angular) { $cacheFactory: $CacheFactoryProvider, $controller: $ControllerProvider, $document: $DocumentProvider, + $$isDocumentHidden: $$IsDocumentHiddenProvider, $exceptionHandler: $ExceptionHandlerProvider, $filter: $FilterProvider, $$forceReflow: $$ForceReflowProvider, @@ -2660,11 +2892,12 @@ function publishExternalAPI(angular) { $window: $WindowProvider, $$rAF: $$RAFProvider, $$jqLite: $$jqLiteProvider, - $$HashMap: $$HashMapProvider, + $$Map: $$MapProvider, $$cookieReader: $$CookieReaderProvider }); } - ]); + ]) + .info({ angularVersion: '1.6.4' }); } /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * @@ -2678,11 +2911,10 @@ function publishExternalAPI(angular) { * Or gives undesired access to variables likes document or window? * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ -/* global JQLitePrototype: true, - addEventListenerFn: true, - removeEventListenerFn: true, +/* global + JQLitePrototype: true, BOOLEAN_ATTR: true, - ALIASED_ATTR: true, + ALIASED_ATTR: true */ ////////////////////////////////// @@ -2724,7 +2956,7 @@ function publishExternalAPI(angular) { * - [`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 + * - [`bind()`](http://api.jquery.com/bind/) (_deprecated_, use [`on()`](http://api.jquery.com/on/)) - Does not support namespaces, selectors or eventData * - [`children()`](http://api.jquery.com/children/) - Does not support selectors * - [`clone()`](http://api.jquery.com/clone/) * - [`contents()`](http://api.jquery.com/contents/) @@ -2744,16 +2976,16 @@ function publishExternalAPI(angular) { * - [`parent()`](http://api.jquery.com/parent/) - Does not support selectors * - [`prepend()`](http://api.jquery.com/prepend/) * - [`prop()`](http://api.jquery.com/prop/) - * - [`ready()`](http://api.jquery.com/ready/) + * - [`ready()`](http://api.jquery.com/ready/) (_deprecated_, use `angular.element(callback)` instead of `angular.element(document).ready(callback)`) * - [`remove()`](http://api.jquery.com/remove/) - * - [`removeAttr()`](http://api.jquery.com/removeAttr/) + * - [`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 + * - [`unbind()`](http://api.jquery.com/unbind/) (_deprecated_, use [`off()`](http://api.jquery.com/off/)) - Does not support namespaces or event object as parameter * - [`val()`](http://api.jquery.com/val/) * - [`wrap()`](http://api.jquery.com/wrap/) * @@ -2791,13 +3023,7 @@ function publishExternalAPI(angular) { 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); - }; + jqId = 1; /* * !!! This is an undocumented "private" function !!! @@ -2810,22 +3036,31 @@ JQLite._data = function(node) { 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 DASH_LOWERCASE_REGEXP = /-([a-z])/g; +var MS_HACK_REGEXP = /^-ms-/; +var MOUSE_EVENT_MAP = { mouseleave: 'mouseout', mouseenter: 'mouseover' }; var jqLiteMinErr = minErr('jqLite'); /** - * Converts snake_case to camelCase. - * Also there is special case for Moz prefix starting with upper case letter. + * Converts kebab-case to camelCase. + * There is also a special case for the ms prefix starting with a lowercase letter. + * @param name Name to normalize + */ +function cssKebabToCamel(name) { + return kebabToCamel(name.replace(MS_HACK_REGEXP, 'ms-')); +} + +function fnCamelCaseReplace(all, letter) { + return letter.toUpperCase(); +} + +/** + * Converts kebab-case to camelCase. * @param name Name to normalize */ -function camelCase(name) { - return name. - replace(SPECIAL_CHARS_REGEXP, function(_, separator, letter, offset) { - return offset ? letter.toUpperCase() : letter; - }). - replace(MOZ_HACK_REGEXP, 'Moz$1'); +function kebabToCamel(name) { + return name + .replace(DASH_LOWERCASE_REGEXP, fnCamelCaseReplace); } var SINGLE_TAG_REGEXP = /^<([\w-]+)\s*\/?>(?:<\/\1>|)$/; @@ -2840,7 +3075,7 @@ var wrapMap = { 'col': [2, '<table><colgroup>', '</colgroup></table>'], 'tr': [2, '<table><tbody>', '</tbody></table>'], 'td': [3, '<table><tbody><tr>', '</tr></tbody></table>'], - '_default': [0, "", ""] + '_default': [0, '', ''] }; wrapMap.optgroup = wrapMap.option; @@ -2866,12 +3101,6 @@ function jqLiteHasData(node) { 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(), @@ -2882,10 +3111,10 @@ function jqLiteBuildFragment(html, context) { 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(); + 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></$2>") + wrap[2]; + tmp.innerHTML = wrap[1] + html.replace(XHTML_TAG_REGEXP, '<$1></$2>') + wrap[2]; // Descend through wrappers to the right content i = wrap[0]; @@ -2896,12 +3125,12 @@ function jqLiteBuildFragment(html, context) { nodes = concat(nodes, tmp.childNodes); tmp = fragment.firstChild; - tmp.textContent = ""; + tmp.textContent = ''; } // Remove wrapper from fragment - fragment.textContent = ""; - fragment.innerHTML = ""; // Clear inner HTML + fragment.textContent = ''; + fragment.innerHTML = ''; // Clear inner HTML forEach(nodes, function(node) { fragment.appendChild(node); }); @@ -2936,10 +3165,9 @@ function jqLiteWrapNode(node, wrapper) { // IE9-11 has no method "contains" in SVG element and in Node.prototype. Bug #10259. -var jqLiteContains = window.Node.prototype.contains || function(arg) { - // jshint bitwise: false +var jqLiteContains = window.Node.prototype.contains || /** @this */ function(arg) { + // eslint-disable-next-line no-bitwise return !!(this.compareDocumentPosition(arg) & 16); - // jshint bitwise: true }; ///////////////////////////////////////////// @@ -2955,7 +3183,7 @@ function JQLite(element) { argIsString = true; } if (!(this instanceof JQLite)) { - if (argIsString && element.charAt(0) != '<') { + 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); @@ -2963,6 +3191,8 @@ function JQLite(element) { if (argIsString) { jqLiteAddNodes(this, jqLiteParseHTML(element)); + } else if (isFunction(element)) { + jqLiteReady(element); } else { jqLiteAddNodes(this, element); } @@ -2973,13 +3203,10 @@ function jqLiteClone(element) { } function jqLiteDealoc(element, onlyDescendants) { - if (!onlyDescendants) jqLiteRemoveData(element); + if (!onlyDescendants && jqLiteAcceptsData(element)) jqLite.cleanData([element]); if (element.querySelectorAll) { - var descendants = element.querySelectorAll('*'); - for (var i = 0, l = descendants.length; i < l; i++) { - jqLiteRemoveData(descendants[i]); - } + jqLite.cleanData(element.querySelectorAll('*')); } } @@ -2995,7 +3222,7 @@ function jqLiteOff(element, type, fn, unsupported) { if (!type) { for (type in events) { if (type !== '$destroy') { - removeEventListenerFn(element, type, handle); + element.removeEventListener(type, handle); } delete events[type]; } @@ -3007,7 +3234,7 @@ function jqLiteOff(element, type, fn, unsupported) { arrayRemove(listenerFns || [], fn); } if (!(isDefined(fn) && listenerFns && listenerFns.length > 0)) { - removeEventListenerFn(element, type, handle); + element.removeEventListener(type, handle); delete events[type]; } }; @@ -3058,6 +3285,7 @@ function jqLiteExpandoStore(element, createIfNecessary) { function jqLiteData(element, key, value) { if (jqLiteAcceptsData(element)) { + var prop; var isSimpleSetter = isDefined(value); var isSimpleGetter = !isSimpleSetter && key && !isObject(key); @@ -3066,16 +3294,18 @@ function jqLiteData(element, key, value) { var data = expandoStore && expandoStore.data; if (isSimpleSetter) { // data('key', value) - data[key] = value; + data[kebabToCamel(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]; + return data && data[kebabToCamel(key)]; } else { // mass-setter: data({key1: val1, key2: val2}) - extend(data, key); + for (prop in key) { + data[kebabToCamel(prop)] = key[prop]; + } } } } @@ -3084,17 +3314,17 @@ function jqLiteData(element, key, value) { function jqLiteHasClass(element, selector) { if (!element.getAttribute) return false; - return ((" " + (element.getAttribute('class') || '') + " ").replace(/[\n\t]/g, " "). - indexOf(" " + selector + " ") > -1); + return ((' ' + (element.getAttribute('class') || '') + ' ').replace(/[\n\t]/g, ' '). + indexOf(' ' + selector + ' ') > -1); } function jqLiteRemoveClass(element, cssClasses) { if (cssClasses && element.setAttribute) { forEach(cssClasses.split(' '), function(cssClass) { element.setAttribute('class', trim( - (" " + (element.getAttribute('class') || '') + " ") - .replace(/[\n\t]/g, " ") - .replace(" " + trim(cssClass) + " ", " ")) + (' ' + (element.getAttribute('class') || '') + ' ') + .replace(/[\n\t]/g, ' ') + .replace(' ' + trim(cssClass) + ' ', ' ')) ); }); } @@ -3103,7 +3333,7 @@ function jqLiteRemoveClass(element, cssClasses) { function jqLiteAddClass(element, cssClasses) { if (cssClasses && element.setAttribute) { var existingClasses = (' ' + (element.getAttribute('class') || '') + ' ') - .replace(/[\n\t]/g, " "); + .replace(/[\n\t]/g, ' '); forEach(cssClasses.split(' '), function(cssClass) { cssClass = trim(cssClass); @@ -3150,7 +3380,7 @@ function jqLiteController(element, name) { 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) { + if (element.nodeType === NODE_TYPE_DOCUMENT) { element = element.documentElement; } var names = isArray(name) ? name : [name]; @@ -3194,30 +3424,32 @@ function jqLiteDocumentLoaded(action, win) { } } +function jqLiteReady(fn) { + function trigger() { + window.document.removeEventListener('DOMContentLoaded', trigger); + window.removeEventListener('load', trigger); + fn(); + } + + // check if document is already loaded + if (window.document.readyState === 'complete') { + window.setTimeout(fn); + } else { + // We can not use jqLite since we are not done loading and jQuery could be loaded later. + + // Works for modern browsers and IE9 + window.document.addEventListener('DOMContentLoaded', trigger); + + // Fallback to window.onload for others + window.addEventListener('load', trigger); + } +} + ////////////////////////////////////////// // Functions which are declared directly. ////////////////////////////////////////// var JQLitePrototype = JQLite.prototype = { - ready: function(fn) { - var fired = false; - - function trigger() { - if (fired) return; - fired = true; - fn(); - } - - // check if document 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. - // jshint -W064 - JQLite(window).on('load', trigger); // fallback to window.onload for others - // jshint +W064 - } - }, + ready: jqLiteReady, toString: function() { var value = []; forEach(this, function(e) { value.push('' + e);}); @@ -3252,7 +3484,8 @@ var ALIASED_ATTR = { 'ngMaxlength': 'maxlength', 'ngMin': 'min', 'ngMax': 'max', - 'ngPattern': 'pattern' + 'ngPattern': 'pattern', + 'ngStep': 'step' }; function getBooleanAttrName(element, name) { @@ -3271,7 +3504,11 @@ forEach({ data: jqLiteData, removeData: jqLiteRemoveData, hasData: jqLiteHasData, - cleanData: jqLiteCleanData + cleanData: function jqLiteCleanData(nodes) { + for (var i = 0, ii = nodes.length; i < ii; i++) { + jqLiteRemoveData(nodes[i]); + } + } }, function(fn, name) { JQLite[name] = fn; }); @@ -3303,7 +3540,7 @@ forEach({ hasClass: jqLiteHasClass, css: function(element, name, value) { - name = camelCase(name); + name = cssKebabToCamel(name); if (isDefined(value)) { element.style[name] = value; @@ -3313,33 +3550,33 @@ forEach({ }, attr: function(element, name, value) { + var ret; var nodeType = element.nodeType; - if (nodeType === NODE_TYPE_TEXT || nodeType === NODE_TYPE_ATTRIBUTE || nodeType === NODE_TYPE_COMMENT) { + if (nodeType === NODE_TYPE_TEXT || nodeType === NODE_TYPE_ATTRIBUTE || nodeType === NODE_TYPE_COMMENT || + !element.getAttribute) { return; } + var lowercasedName = lowercase(name); - if (BOOLEAN_ATTR[lowercasedName]) { - if (isDefined(value)) { - if (!!value) { - element[name] = true; - element.setAttribute(name, lowercasedName); - } else { - element[name] = false; - element.removeAttribute(lowercasedName); - } + var isBooleanAttr = BOOLEAN_ATTR[lowercasedName]; + + if (isDefined(value)) { + // setter + + if (value === null || (value === false && isBooleanAttr)) { + element.removeAttribute(name); } else { - return (element[name] || - (element.attributes.getNamedItem(name) || noop).specified) - ? lowercasedName - : undefined; - } - } 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) + element.setAttribute(name, isBooleanAttr ? lowercasedName : value); + } + } else { + // getter + + ret = element.getAttribute(name); + + if (isBooleanAttr && ret !== null) { + ret = lowercasedName; + } + // Normalize non-existing attributes to undefined (as jQuery). return ret === null ? undefined : ret; } }, @@ -3374,7 +3611,7 @@ forEach({ result.push(option.value || option.text); } }); - return result.length === 0 ? null : result; + return result; } return element.value; } @@ -3402,7 +3639,7 @@ forEach({ // 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))) { + (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 @@ -3544,7 +3781,7 @@ forEach({ eventFns = events[type] = []; eventFns.specialHandlerWrapper = specialHandlerWrapper; if (type !== '$destroy' && !noEventListener) { - addEventListenerFn(element, type, handle); + element.addEventListener(type, handle); } } @@ -3637,12 +3874,15 @@ forEach({ after: function(element, newElement) { var index = element, parent = element.parentNode; - 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; + 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; + } } }, @@ -3736,14 +3976,15 @@ forEach({ } return isDefined(value) ? value : this; }; - - // bind legacy bind/unbind to on/off - JQLite.prototype.bind = JQLite.prototype.on; - JQLite.prototype.unbind = JQLite.prototype.off; }); +// bind legacy bind/unbind to on/off +JQLite.prototype.bind = JQLite.prototype.on; +JQLite.prototype.unbind = JQLite.prototype.off; + // Provider for private $$jqLite service +/** @this */ function $$jqLiteProvider() { this.$get = function $$jqLite() { return extend(JQLite, { @@ -3786,7 +4027,7 @@ function hashKey(obj, nextUidFn) { } var objType = typeof obj; - if (objType == 'function' || (objType == 'object' && obj !== null)) { + if (objType === 'function' || (objType === 'object' && obj !== null)) { key = obj.$$hashKey = objType + ':' + (nextUidFn || nextUid)(); } else { key = objType + ':' + obj; @@ -3795,50 +4036,70 @@ function hashKey(obj, nextUidFn) { 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); +// A minimal ES2015 Map implementation. +// Should be bug/feature equivalent to the native implementations of supported browsers +// (for the features required in Angular). +// See https://kangax.github.io/compat-table/es6/#test-Map +var nanKey = Object.create(null); +function NgMapShim() { + this._keys = []; + this._values = []; + this._lastKey = NaN; + this._lastIndex = -1; } -HashMap.prototype = { - /** - * Store key value pair - * @param key key to store can be any type - * @param value value to store can be any type - */ - put: function(key, value) { - this[hashKey(key, this.nextUid)] = value; +NgMapShim.prototype = { + _idx: function(key) { + if (key === this._lastKey) { + return this._lastIndex; + } + this._lastKey = key; + this._lastIndex = this._keys.indexOf(key); + return this._lastIndex; + }, + _transformKey: function(key) { + return isNumberNaN(key) ? nanKey : key; }, - - /** - * @param key - * @returns {Object} the value for the key - */ get: function(key) { - return this[hashKey(key, this.nextUid)]; + key = this._transformKey(key); + var idx = this._idx(key); + if (idx !== -1) { + return this._values[idx]; + } }, + set: function(key, value) { + key = this._transformKey(key); + var idx = this._idx(key); + if (idx === -1) { + idx = this._lastIndex = this._keys.length; + } + this._keys[idx] = key; + this._values[idx] = value; - /** - * Remove the key/value pair - * @param key - */ - remove: function(key) { - var value = this[key = hashKey(key, this.nextUid)]; - delete this[key]; - return value; + // Support: IE11 + // Do not `return this` to simulate the partial IE11 implementation + }, + delete: function(key) { + key = this._transformKey(key); + var idx = this._idx(key); + if (idx === -1) { + return false; + } + this._keys.splice(idx, 1); + this._values.splice(idx, 1); + this._lastKey = NaN; + this._lastIndex = -1; + return true; } }; -var $$HashMapProvider = [function() { +// For now, always use `NgMapShim`, even if `window.Map` is available. Some native implementations +// are still buggy (often in subtle ways) and can cause hard-to-debug failures. When native `Map` +// implementations get more stable, we can reconsider switching to `window.Map` (when available). +var NgMap = NgMapShim; + +var $$MapProvider = [/** @this */function() { this.$get = [function() { - return HashMap; + return NgMap; }]; }]; @@ -3905,19 +4166,15 @@ var $$HashMapProvider = [function() { * Implicit module which gets automatically added to each {@link auto.$injector $injector}. */ -var ARROW_ARG = /^([^\(]+?)=>/; -var FN_ARGS = /^[^\(]*\(\s*([^\)]*)\)/m; +var ARROW_ARG = /^([^(]+?)=>/; +var FN_ARGS = /^[^(]*\(\s*([^)]*)\)/m; var FN_ARG_SPLIT = /,/; var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/; var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg; var $injectorMinErr = minErr('$injector'); function 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) + ' '; + return Function.prototype.toString.call(fn); } function extractArgs(fn) { @@ -4027,6 +4284,28 @@ function annotate(fn, strictDi, name) { */ /** + * @ngdoc property + * @name $injector#modules + * @type {Object} + * @description + * A hash containing all the modules that have been loaded into the + * $injector. + * + * You can use this property to find out information about a module via the + * {@link angular.Module#info `myModule.info(...)`} method. + * + * For example: + * + * ``` + * var info = $injector.modules['ngAnimate'].info(); + * ``` + * + * **Do not use this property to attempt to modify the modules after the application + * has been bootstrapped.** + */ + + +/** * @ngdoc method * @name $injector#get * @@ -4164,7 +4443,6 @@ function annotate(fn, strictDi, name) { - /** * @ngdoc service * @name $provide @@ -4492,7 +4770,7 @@ function createInjector(modulesToLoad, strictDi) { var INSTANTIATING = {}, providerSuffix = 'Provider', path = [], - loadedModules = new HashMap([], true), + loadedModules = new NgMap(), providerCache = { $provide: { provider: supportObject(provider), @@ -4508,7 +4786,7 @@ function createInjector(modulesToLoad, strictDi) { if (angular.isString(caller)) { path.push(caller); } - throw $injectorMinErr('unpr', "Unknown provider: {0}", path.join(' <- ')); + throw $injectorMinErr('unpr', 'Unknown provider: {0}', path.join(' <- ')); })), instanceCache = {}, protoInstanceInjector = @@ -4520,6 +4798,7 @@ function createInjector(modulesToLoad, strictDi) { instanceInjector = protoInstanceInjector; providerCache['$injector' + providerSuffix] = { $get: valueFn(protoInstanceInjector) }; + instanceInjector.modules = providerInjector.modules = createMap(); var runBlocks = loadModules(modulesToLoad); instanceInjector = protoInstanceInjector.get('$injector'); instanceInjector.strictDi = strictDi; @@ -4547,16 +4826,16 @@ function createInjector(modulesToLoad, strictDi) { provider_ = providerInjector.instantiate(provider_); } if (!provider_.$get) { - throw $injectorMinErr('pget', "Provider '{0}' must define $get factory method.", name); + throw $injectorMinErr('pget', 'Provider \'{0}\' must define $get factory method.', name); } - return providerCache[name + providerSuffix] = provider_; + return (providerCache[name + providerSuffix] = provider_); } function enforceReturnValue(name, factory) { - return function enforcedReturnValue() { + 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); + throw $injectorMinErr('undef', 'Provider \'{0}\' must return a value from $get factory method.', name); } return result; }; @@ -4600,7 +4879,7 @@ function createInjector(modulesToLoad, strictDi) { var runBlocks = [], moduleFn; forEach(modulesToLoad, function(module) { if (loadedModules.get(module)) return; - loadedModules.put(module, true); + loadedModules.set(module, true); function runInvokeQueue(queue) { var i, ii; @@ -4615,6 +4894,7 @@ function createInjector(modulesToLoad, strictDi) { try { if (isString(module)) { moduleFn = angularModule(module); + instanceInjector.modules[module] = moduleFn; runBlocks = runBlocks.concat(loadModules(moduleFn.requires)).concat(moduleFn._runBlocks); runInvokeQueue(moduleFn._invokeQueue); runInvokeQueue(moduleFn._configBlocks); @@ -4629,15 +4909,15 @@ function createInjector(modulesToLoad, strictDi) { if (isArray(module)) { module = module[module.length - 1]; } - if (e.message && e.stack && e.stack.indexOf(e.message) == -1) { + if (e.message && e.stack && e.stack.indexOf(e.message) === -1) { // Safari & FF's stack traces don't contain error.message content // unlike those of Chrome and IE // So if stack doesn't contain message, we create a new string that contains both. // Since error.stack is read-only in Safari, I'm overriding e and not e.stack here. - /* jshint -W022 */ + // eslint-disable-next-line no-ex-assign e = e.message + '\n' + e.stack; } - throw $injectorMinErr('modulerr', "Failed to instantiate module {0} due to:\n{1}", + throw $injectorMinErr('modulerr', 'Failed to instantiate module {0} due to:\n{1}', module, e.stack || e.message || e); } }); @@ -4661,7 +4941,8 @@ function createInjector(modulesToLoad, strictDi) { try { path.unshift(serviceName); cache[serviceName] = INSTANTIATING; - return cache[serviceName] = factory(serviceName, caller); + cache[serviceName] = factory(serviceName, caller); + return cache[serviceName]; } catch (err) { if (cache[serviceName] === INSTANTIATING) { delete cache[serviceName]; @@ -4691,14 +4972,18 @@ function createInjector(modulesToLoad, strictDi) { } function isClass(func) { + // Support: IE 9-11 only // IE 9-11 do not support classes and IE9 leaks with the code below. - if (msie <= 11) { + if (msie || typeof func !== 'function') { 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)); + var result = func.$$ngIsClass; + if (!isBoolean(result)) { + // Support: Edge 12-13 only + // See: https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/6156135/ + result = func.$$ngIsClass = /^(?:class\b|constructor\()/.test(stringifyFn(func)); + } + return result; } function invoke(fn, self, locals, serviceName) { @@ -4751,6 +5036,7 @@ createInjector.$$annotate = annotate; /** * @ngdoc provider * @name $anchorScrollProvider + * @this * * @description * Use `$anchorScrollProvider` to disable automatic scrolling whenever @@ -4822,7 +5108,7 @@ function $AnchorScrollProvider() { * </div> * * @example - <example module="anchorScrollExample"> + <example module="anchorScrollExample" name="anchor-scroll"> <file name="index.html"> <div id="scrollArea" ng-controller="ScrollController"> <a ng-click="gotoBottom()">Go to bottom</a> @@ -4832,7 +5118,7 @@ function $AnchorScrollProvider() { <file name="script.js"> angular.module('anchorScrollExample', []) .controller('ScrollController', ['$scope', '$location', '$anchorScroll', - function ($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. @@ -4861,7 +5147,7 @@ function $AnchorScrollProvider() { * See {@link ng.$anchorScroll#yOffset $anchorScroll.yOffset} for more details. * * @example - <example module="anchorScrollOffsetExample"> + <example module="anchorScrollOffsetExample" name="anchor-scroll-offset"> <file name="index.html"> <div class="fixed-header" ng-controller="headerCtrl"> <a href="" ng-click="gotoAnchor(x)" ng-repeat="x in [1,2,3,4,5]"> @@ -4878,7 +5164,7 @@ function $AnchorScrollProvider() { $anchorScroll.yOffset = 50; // always scroll by 50 extra pixels }]) .controller('headerCtrl', ['$anchorScroll', '$location', '$scope', - function ($anchorScroll, $location, $scope) { + function($anchorScroll, $location, $scope) { $scope.gotoAnchor = function(x) { var newHash = 'anchor' + x; if ($location.hash() !== newHash) { @@ -4985,7 +5271,8 @@ function $AnchorScrollProvider() { } function scroll(hash) { - hash = isString(hash) ? hash : $location.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 @@ -4997,7 +5284,7 @@ function $AnchorScrollProvider() { // 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 + // no element and hash === 'top', scroll to the top of the page else if (hash === 'top') scrollTo(null); } @@ -5072,14 +5359,14 @@ function prepareAnimateOptions(options) { : {}; } -var $$CoreAnimateJsProvider = function() { +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 = function() { - var postDigestQueue = new HashMap(); +var $$CoreAnimateQueueProvider = /** @this */ function() { + var postDigestQueue = new NgMap(); var postDigestElements = []; this.$get = ['$$AnimateRunner', '$rootScope', @@ -5091,17 +5378,23 @@ var $$CoreAnimateQueueProvider = function() { pin: noop, push: function(element, event, options, domOperation) { - domOperation && domOperation(); + if (domOperation) { + domOperation(); + } options = options || {}; - options.from && element.css(options.from); - options.to && element.css(options.to); + 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(); // jshint ignore:line + var runner = new $$AnimateRunner(); // since there are no animations to run the runner needs to be // notified that the animation call is complete. @@ -5145,10 +5438,14 @@ var $$CoreAnimateQueueProvider = function() { }); forEach(element, function(elm) { - toAdd && jqLiteAddClass(elm, toAdd); - toRemove && jqLiteRemoveClass(elm, toRemove); + if (toAdd) { + jqLiteAddClass(elm, toAdd); + } + if (toRemove) { + jqLiteRemoveClass(elm, toRemove); + } }); - postDigestQueue.remove(element); + postDigestQueue.delete(element); } }); postDigestElements.length = 0; @@ -5163,7 +5460,7 @@ var $$CoreAnimateQueueProvider = function() { if (classesAdded || classesRemoved) { - postDigestQueue.put(element, data); + postDigestQueue.set(element, data); postDigestElements.push(element); if (postDigestElements.length === 1) { @@ -5186,8 +5483,9 @@ var $$CoreAnimateQueueProvider = function() { * * To see the functional implementation check out `src/ngAnimate/animate.js`. */ -var $AnimateProvider = ['$provide', function($provide) { +var $AnimateProvider = ['$provide', /** @this */ function($provide) { var provider = this; + var classNameFilter = null; this.$$registeredAnimations = Object.create(null); @@ -5232,7 +5530,7 @@ var $AnimateProvider = ['$provide', function($provide) { */ this.register = function(name, factory) { if (name && name.charAt(0) !== '.') { - throw $animateMinErr('notcsel', "Expecting class selector starting with '.' got '{0}'.", name); + throw $animateMinErr('notcsel', 'Expecting class selector starting with \'.\' got \'{0}\'.', name); } var key = name + '-animation'; @@ -5256,16 +5554,16 @@ var $AnimateProvider = ['$provide', function($provide) { */ 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); - + classNameFilter = (expression instanceof RegExp) ? expression : null; + if (classNameFilter) { + var reservedRegex = new RegExp('[(\\s|\\/)]' + NG_ANIMATE_CLASSNAME + '[(\\s|\\/)]'); + if (reservedRegex.test(classNameFilter.toString())) { + classNameFilter = null; + throw $animateMinErr('nongcls', '$animateProvider.classNameFilter(regex) prohibits accepting a regex value which matches/contains the "{0}" CSS class.', NG_ANIMATE_CLASSNAME); } } } - return this.$$classNameFilter; + return classNameFilter; }; this.$get = ['$$animateQueue', function($$animateQueue) { @@ -5279,7 +5577,11 @@ var $AnimateProvider = ['$provide', function($provide) { afterElement = null; } } - afterElement ? afterElement.after(element) : parentElement.prepend(element); + if (afterElement) { + afterElement.after(element); + } else { + parentElement.prepend(element); + } } /** @@ -5422,7 +5724,9 @@ var $AnimateProvider = ['$provide', function($provide) { * @param {Promise} animationPromise The animation promise that is returned when an animation is started. */ cancel: function(runner) { - runner.end && runner.end(); + if (runner.end) { + runner.end(); + } }, /** @@ -5613,7 +5917,7 @@ var $AnimateProvider = ['$provide', function($provide) { * * @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 classNamem, then the provided `from` and + * 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` @@ -5659,7 +5963,7 @@ var $AnimateProvider = ['$provide', function($provide) { }]; }]; -var $$AnimateAsyncRunFactoryProvider = function() { +var $$AnimateAsyncRunFactoryProvider = /** @this */ function() { this.$get = ['$$rAF', function($$rAF) { var waitQueue = []; @@ -5680,15 +5984,19 @@ var $$AnimateAsyncRunFactoryProvider = function() { passed = true; }); return function(callback) { - passed ? callback() : waitForTick(callback); + if (passed) { + callback(); + } else { + waitForTick(callback); + } }; }; }]; }; -var $$AnimateRunnerFactoryProvider = function() { - this.$get = ['$q', '$sniffer', '$$animateAsyncRun', '$document', '$timeout', - function($q, $sniffer, $$animateAsyncRun, $document, $timeout) { +var $$AnimateRunnerFactoryProvider = /** @this */ function() { + this.$get = ['$q', '$sniffer', '$$animateAsyncRun', '$$isDocumentHidden', '$timeout', + function($q, $sniffer, $$animateAsyncRun, $$isDocumentHidden, $timeout) { var INITIAL_STATE = 0; var DONE_PENDING_STATE = 1; @@ -5740,11 +6048,7 @@ var $$AnimateRunnerFactoryProvider = function() { 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) { + if ($$isDocumentHidden()) { timeoutTick(fn); } else { rafTick(fn); @@ -5773,7 +6077,11 @@ var $$AnimateRunnerFactoryProvider = function() { var self = this; this.promise = $q(function(resolve, reject) { self.done(function(status) { - status === false ? reject() : resolve(); + if (status === false) { + reject(); + } else { + resolve(); + } }); }); } @@ -5843,10 +6151,13 @@ var $$AnimateRunnerFactoryProvider = function() { }]; }; +/* 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, @@ -5879,7 +6190,6 @@ var $CoreAnimateCssProvider = function() { options.from = null; } - /* jshint newcap: false */ var closed, runner = new $$AnimateRunner(); return { start: run, @@ -6012,7 +6322,6 @@ function Browser(window, document, $log, $sniffer) { }; cacheState(); - lastHistoryState = cachedState; /** * @name $browser#url @@ -6066,8 +6375,6 @@ function Browser(window, document, $log, $sniffer) { 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; @@ -6093,7 +6400,7 @@ function Browser(window, document, $log, $sniffer) { // 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,"'"); + return pendingLocation || location.href.replace(/%27/g,'\''); } }; @@ -6116,8 +6423,7 @@ function Browser(window, document, $log, $sniffer) { function cacheStateAndFireUrlChange() { pendingLocation = null; - cacheState(); - fireUrlChange(); + fireStateOrUrlChange(); } // This variable should be used *only* inside the cacheState function. @@ -6131,11 +6437,16 @@ function Browser(window, document, $log, $sniffer) { if (equals(cachedState, lastCachedState)) { cachedState = lastCachedState; } + lastCachedState = cachedState; + lastHistoryState = cachedState; } - function fireUrlChange() { - if (lastBrowserUrl === self.url() && lastHistoryState === cachedState) { + function fireStateOrUrlChange() { + var prevLastHistoryState = lastHistoryState; + cacheState(); + + if (lastBrowserUrl === self.url() && prevLastHistoryState === cachedState) { return; } @@ -6170,8 +6481,8 @@ function Browser(window, document, $log, $sniffer) { self.onUrlChange = function(callback) { // TODO(vojta): refactor to use node's syntax for events if (!urlChangeInit) { - // We listen on both (hashchange/popstate) when available, as some browsers (e.g. Opera) - // don't fire popstate when user change the address bar and don't fire hashchange when url + // We listen on both (hashchange/popstate) when available, as some browsers don't + // fire popstate when user changes the address bar and don't fire hashchange when url // changed by push/replaceState // html5 history api - popstate event @@ -6201,7 +6512,7 @@ function Browser(window, document, $log, $sniffer) { * 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; + self.$$checkUrlChange = fireStateOrUrlChange; ////////////////////////////////////////////////////////////// // Misc API @@ -6218,7 +6529,7 @@ function Browser(window, document, $log, $sniffer) { */ self.baseHref = function() { var href = baseElement.attr('href'); - return href ? href.replace(/^(https?\:)?\/\/[^\/]*/, '') : ''; + return href ? href.replace(/^(https?:)?\/\/[^/]*/, '') : ''; }; /** @@ -6269,6 +6580,7 @@ function Browser(window, document, $log, $sniffer) { } +/** @this */ function $BrowserProvider() { this.$get = ['$window', '$log', '$sniffer', '$document', function($window, $log, $sniffer, $document) { @@ -6279,6 +6591,7 @@ function $BrowserProvider() { /** * @ngdoc service * @name $cacheFactory + * @this * * @description * Factory that constructs {@link $cacheFactory.Cache Cache} objects and gives access to @@ -6315,7 +6628,7 @@ function $BrowserProvider() { * - `{void}` `destroy()` — Removes references to this cache from $cacheFactory. * * @example - <example module="cacheExampleApp"> + <example module="cacheExampleApp" name="cache-factory"> <file name="index.html"> <div ng-controller="CacheController"> <input ng-model="newCacheKey" placeholder="Key"> @@ -6364,7 +6677,7 @@ function $CacheFactoryProvider() { function cacheFactory(cacheId, options) { if (cacheId in caches) { - throw minErr('$cacheFactory')('iid', "CacheId '{0}' is already taken!", cacheId); + throw minErr('$cacheFactory')('iid', 'CacheId \'{0}\' is already taken!', cacheId); } var size = 0, @@ -6414,7 +6727,7 @@ function $CacheFactoryProvider() { * })); * ``` */ - return caches[cacheId] = { + return (caches[cacheId] = { /** * @ngdoc method @@ -6492,8 +6805,8 @@ function $CacheFactoryProvider() { if (!lruEntry) return; - if (lruEntry == freshEnd) freshEnd = lruEntry.p; - if (lruEntry == staleEnd) staleEnd = lruEntry.n; + if (lruEntry === freshEnd) freshEnd = lruEntry.p; + if (lruEntry === staleEnd) staleEnd = lruEntry.n; link(lruEntry.n,lruEntry.p); delete lruHash[key]; @@ -6558,17 +6871,17 @@ function $CacheFactoryProvider() { info: function() { return extend({}, stats, {size: size}); } - }; + }); /** * makes the `entry` the freshEnd of the LRU linked list */ function refresh(entry) { - if (entry != freshEnd) { + if (entry !== freshEnd) { if (!staleEnd) { staleEnd = entry; - } else if (staleEnd == entry) { + } else if (staleEnd === entry) { staleEnd = entry.n; } @@ -6584,7 +6897,7 @@ function $CacheFactoryProvider() { * bidirectionally links two entries of the LRU linked list */ function link(nextEntry, prevEntry) { - if (nextEntry != prevEntry) { + if (nextEntry !== prevEntry) { if (nextEntry) nextEntry.p = prevEntry; //p stands for previous, 'prev' didn't minify if (prevEntry) prevEntry.n = nextEntry; //n stands for next, 'next' didn't minify } @@ -6632,6 +6945,7 @@ function $CacheFactoryProvider() { /** * @ngdoc service * @name $templateCache + * @this * * @description * The first time a template is used, it is loaded in the template cache for quick retrieval. You @@ -6659,12 +6973,14 @@ function $CacheFactoryProvider() { * }); * ``` * - * To retrieve the template later, simply use it in your HTML: - * ```html - * <div ng-include=" 'templateId.html' "></div> + * To retrieve the template later, simply use it in your component: + * ```js + * myApp.component('myComponent', { + * templateUrl: 'templateId.html' + * }); * ``` * - * or get it via Javascript: + * or get it via the `$templateCache` service: * ```js * $templateCache.get('templateId.html') * ``` @@ -6686,7 +7002,7 @@ function $TemplateCacheProvider() { * * * 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? * + * Or gives undesired access to variables like document or window? * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ /* ! VARIABLE/FUNCTION NAMING CONVENTIONS THAT APPLY TO THIS FILE! @@ -6745,33 +7061,34 @@ function $TemplateCacheProvider() { * * myModule.directive('directiveName', function factory(injectables) { * var directiveDefinitionObject = { - * priority: 0, - * template: '<div></div>', // or // function(tElement, tAttrs) { ... }, + * {@link $compile#-priority- priority}: 0, + * {@link $compile#-template- template}: '<div></div>', // or // function(tElement, tAttrs) { ... }, * // or - * // templateUrl: 'directive.html', // or // function(tElement, tAttrs) { ... }, - * transclude: false, - * restrict: 'A', - * templateNamespace: 'html', - * scope: false, - * controller: function($scope, $element, $attrs, $transclude, otherInjectables) { ... }, - * controllerAs: 'stringIdentifier', - * bindToController: false, - * require: 'siblingDirectiveName', // or // ['^parentDirectiveName', '?optionalDirectiveName', '?^optionalParent'], - * compile: function compile(tElement, tAttrs, transclude) { + * // {@link $compile#-templateurl- templateUrl}: 'directive.html', // or // function(tElement, tAttrs) { ... }, + * {@link $compile#-transclude- transclude}: false, + * {@link $compile#-restrict- restrict}: 'A', + * {@link $compile#-templatenamespace- templateNamespace}: 'html', + * {@link $compile#-scope- scope}: false, + * {@link $compile#-controller- controller}: function($scope, $element, $attrs, $transclude, otherInjectables) { ... }, + * {@link $compile#-controlleras- controllerAs}: 'stringIdentifier', + * {@link $compile#-bindtocontroller- bindToController}: false, + * {@link $compile#-require- require}: 'siblingDirectiveName', // or // ['^parentDirectiveName', '?optionalDirectiveName', '?^optionalParent'], + * {@link $compile#-multielement- multiElement}: false, + * {@link $compile#-compile- compile}: function compile(tElement, tAttrs, transclude) { * return { - * pre: function preLink(scope, iElement, iAttrs, controller) { ... }, - * post: function postLink(scope, iElement, iAttrs, controller) { ... } + * {@link $compile#pre-linking-function pre}: function preLink(scope, iElement, iAttrs, controller) { ... }, + * {@link $compile#post-linking-function post}: function postLink(scope, iElement, iAttrs, controller) { ... } * } * // or * // return function postLink( ... ) { ... } * }, * // or - * // link: { - * // pre: function preLink(scope, iElement, iAttrs, controller) { ... }, - * // post: function postLink(scope, iElement, iAttrs, controller) { ... } + * // {@link $compile#-link- link}: { + * // {@link $compile#pre-linking-function pre}: function preLink(scope, iElement, iAttrs, controller) { ... }, + * // {@link $compile#post-linking-function post}: function postLink(scope, iElement, iAttrs, controller) { ... } * // } * // or - * // link: function postLink( ... ) { ... } + * // {@link $compile#-link- link}: function postLink( ... ) { ... } * }; * return directiveDefinitionObject; * }); @@ -6805,7 +7122,8 @@ function $TemplateCacheProvider() { * * `$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. + * component such as cloning the bound value to prevent accidental mutation of the outer value. Note that this will + * also be called when your bindings are initialized. * * `$doCheck()` - Called on each turn of the digest cycle. Provides an opportunity to detect and act on * changes. Any actions that you wish to take in response to the changes that you detect must be * invoked from this hook; implementing this has no effect on when `$onChanges` is called. For example, this hook @@ -6923,7 +7241,7 @@ function $TemplateCacheProvider() { * compiler}. The attributes are: * * #### `multiElement` - * When this property is set to true, the HTML compiler will collect DOM nodes between + * 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 @@ -6944,19 +7262,21 @@ function $TemplateCacheProvider() { * and other directives used in the directive's template will also be excluded from execution. * * #### `scope` - * The scope property can be `true`, an object or a falsy value: + * The scope property can be `false`, `true`, or an object: * - * * **falsy:** No scope will be created for the directive. The directive will use its parent's scope. + * * **`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. The new scope rule does not apply for the root of the template - * since the root of the template always gets 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. + * * **`{...}` (an object hash):** A new "isolate" scope is created for the directive's template. + * The 'isolate' scope differs from normal scope in that it does not prototypically + * inherit from its parent scope. This is useful when creating reusable components, which should not + * accidentally read or modify data in the parent scope. Note that an isolate scope + * directive without a `template` or `templateUrl` will not apply the isolate scope + * to its children elements. * * The 'isolate' scope object hash defines a set of local scope properties derived from attributes on the * directive's element. These local properties are useful for aliasing values for templates. The keys in @@ -7038,9 +7358,7 @@ function $TemplateCacheProvider() { * * #### `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. Additionally, a controller - * alias must be set, either by using `controllerAs: 'myAlias'` or by specifying the alias in the controller - * definition: `controller: 'myCtrl as myAlias'`. + * `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. @@ -7051,9 +7369,9 @@ function $TemplateCacheProvider() { * initialized. * * <div class="alert alert-warning"> - * **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. + * **Deprecation warning:** if `$compileProcvider.preAssignBindingsEnabled(true)` was called, bindings for non-ES6 class + * controllers are bound to `this` before the controller constructor is called but this use is now deprecated. Please + * place initialization code that relies upon bindings inside a `$onInit` method on the controller, instead. * </div> * * It is also possible to set `bindToController` to an object hash with the same format as the `scope` property. @@ -7081,12 +7399,12 @@ function $TemplateCacheProvider() { * * 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 `cloneLinkinFn` is passed, + * 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 `<svg>`). * * 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 translusion is provided. + * 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). * @@ -7375,7 +7693,7 @@ function $TemplateCacheProvider() { * * 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, to which the clone is bound. + * content and the `scope` is the newly created transclusion scope, which the clone will be linked to. * * <div class="alert alert-info"> * **Best Practice**: Always provide a `cloneFn` (clone attach function) when you call a transclude function @@ -7493,7 +7811,7 @@ function $TemplateCacheProvider() { * to illustrate how `$compile` works. * </div> * - <example module="compileExample"> + <example module="compileExample" name="compile"> <file name="index.html"> <script> angular.module('compileExample', [], function($compileProvider) { @@ -7622,6 +7940,16 @@ function $TemplateCacheProvider() { * * 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'); @@ -7636,11 +7964,12 @@ var _UNINITIALIZED_VALUE = new UNINITIALIZED_VALUE(); * @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\-]+)(?:\:([^;]+))?;?)/, + COMMENT_DIRECTIVE_REGEXP = /^\s*directive:\s*([\w-]+)\s+(.*)$/, + CLASS_DIRECTIVE_REGEXP = /(([\w-]+)(?::([^;]+))?;?)/, ALL_OR_NOTHING_ATTRS = makeMap('ngSrc,ngSrcset,src,srcset'), REQUIRE_PREFIX_REGEXP = /^(?:(\^\^?)?(\?)?(\^\^?)?)?/; @@ -7651,7 +7980,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { var bindingCache = createMap(); function parseIsolateBindings(scope, directiveName, isController) { - var LOCAL_REGEXP = /^\s*([@&<]|=(\*?))(\??)\s*(\w*)\s*$/; + var LOCAL_REGEXP = /^\s*([@&<]|=(\*?))(\??)\s*([\w$]*)\s*$/; var bindings = createMap(); @@ -7664,11 +7993,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (!match) { throw $compileMinErr('iscp', - "Invalid {3} for directive '{0}'." + - " Definition: {... {1}: '{2}' ...}", + 'Invalid {3} for directive \'{0}\'.' + + ' Definition: {... {1}: \'{2}\' ...}', directiveName, scopeName, definition, - (isController ? "controller bindings definition" : - "isolate scope definition")); + (isController ? 'controller bindings definition' : + 'isolate scope definition')); } bindings[scopeName] = { @@ -7704,20 +8033,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { bindings.bindToController = parseIsolateBindings(directive.bindToController, directiveName, true); } - if (isObject(bindings.bindToController)) { - var controller = directive.controller; - var controllerAs = directive.controllerAs; - if (!controller) { - // There is no controller, there may or may not be a controllerAs property - throw $compileMinErr('noctrl', - "Cannot bind to controller without directive '{0}'s controller.", - directiveName); - } else if (!identifierForController(controller, controllerAs)) { - // There is a controller, but no identifier or controllerAs property - throw $compileMinErr('noident', - "Cannot bind to controller without identifier for directive '{0}'.", - directiveName); - } + if (bindings.bindToController && !directive.controller) { + // There is no controller + throw $compileMinErr('noctrl', + 'Cannot bind to controller without directive \'{0}\'s controller.', + directiveName); } return bindings; } @@ -7725,11 +8045,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { 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); + 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", + 'Directive/Component name \'{0}\' is invalid. The name should not contain leading or trailing whitespaces', name); } } @@ -7748,6 +8068,17 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { 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 @@ -7764,6 +8095,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { * @returns {ng.$compileProvider} Self for chaining. */ this.directive = function registerDirective(name, directiveFactory) { + assertArg(name, 'name'); assertNotHasOwnProperty(name, 'directive'); if (isString(name)) { assertValidDirectiveName(name); @@ -7785,7 +8117,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { directive.index = index; directive.name = directive.name || name; directive.require = getDirectiveRequire(directive); - directive.restrict = directive.restrict || 'EA'; + directive.restrict = getDirectiveRestrict(directive.restrict, name); directive.$$moduleName = directiveFactory.$$moduleName; directives.push(directive); } catch (e) { @@ -7894,7 +8226,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { function factory($injector) { function makeInjectable(fn) { if (isFunction(fn) || isArray(fn)) { - return function(tElement, tAttrs) { + return /** @this */ function(tElement, tAttrs) { return $injector.invoke(fn, this, {$element: tElement, $attrs: tAttrs}); }; } else { @@ -8034,6 +8366,42 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { return debugInfoEnabled; }; + /** + * @ngdoc method + * @name $compileProvider#preAssignBindingsEnabled + * + * @param {boolean=} enabled update the preAssignBindingsEnabled state if provided, otherwise just return the + * current preAssignBindingsEnabled state + * @returns {*} current value if used as getter or itself (chaining) if used as setter + * + * @kind function + * + * @description + * Call this method to enable/disable whether directive controllers are assigned bindings before + * calling the controller's constructor. + * If enabled (true), the compiler assigns the value of each of the bindings to the + * properties of the controller object before the constructor of this object is called. + * + * If disabled (false), the compiler calls the constructor first before assigning bindings. + * + * The default value is false. + * + * @deprecated + * sinceVersion="1.6.0" + * removeVersion="1.7.0" + * + * This method and the option to assign the bindings before calling the controller's constructor + * will be removed in v1.7.0. + */ + var preAssignBindingsEnabled = false; + this.preAssignBindingsEnabled = function(enabled) { + if (isDefined(enabled)) { + preAssignBindingsEnabled = enabled; + return this; + } + return preAssignBindingsEnabled; + }; + var TTL = 10; /** @@ -8064,6 +8432,63 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { 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', @@ -8074,6 +8499,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { 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 @@ -8251,7 +8679,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { this[key] = value = $$sanitizeUri(value, key === 'src'); } else if (nodeName === 'img' && key === 'srcset' && isDefined(value)) { // sanitize img[srcset] values - var result = ""; + var result = ''; // first check if there are spaces because it's not the same pattern var trimmedSrcset = trim(value); @@ -8269,7 +8697,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { // sanitize the uri result += $$sanitizeUri(trim(rawUris[innerIdx]), true); // add the descriptor - result += (" " + trim(rawUris[innerIdx + 1])); + result += (' ' + trim(rawUris[innerIdx + 1])); } // split the last item into uri and descriptor @@ -8280,7 +8708,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { // and add the last descriptor if any if (lastTuple.length === 2) { - result += (" " + trim(lastTuple[1])); + result += (' ' + trim(lastTuple[1])); } this[key] = value = result; } @@ -8299,13 +8727,15 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { // fire observers var $$observers = this.$$observers; - $$observers && forEach($$observers[observer], function(fn) { - try { - fn(value); - } catch (e) { - $exceptionHandler(e); - } - }); + if ($$observers) { + forEach($$observers[observer], function(fn) { + try { + fn(value); + } catch (e) { + $exceptionHandler(e); + } + }); + } }, @@ -8351,7 +8781,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { // 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 = "<span " + attrName + ">"; + specialAttrHolder.innerHTML = '<span ' + attrName + '>'; 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 @@ -8372,7 +8802,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { var startSymbol = $interpolate.startSymbol(), endSymbol = $interpolate.endSymbol(), - denormalizeTemplate = (startSymbol == '{{' && endSymbol == '}}') + denormalizeTemplate = (startSymbol === '{{' && endSymbol === '}}') ? identity : function denormalizeTemplate(template) { return template.replace(/\{\{/g, startSymbol).replace(/}}/g, endSymbol); @@ -8425,25 +8855,15 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { // 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 <span> - 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) { + if (!$compileNodes) { + throw $compileMinErr('multilink', 'This element has already been linked.'); + } assertArg(scope, 'scope'); if (previousCompileContext && previousCompileContext.needsNewScope) { @@ -8498,6 +8918,10 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (cloneConnectFn) cloneConnectFn($linkNode, scope); if (compositeLinkFn) compositeLinkFn(scope, $linkNode, $linkNode, parentBoundTranscludeFn); + + if (!cloneConnectFn) { + $compileNodes = compositeLinkFn = null; + } return $linkNode; }; } @@ -8530,12 +8954,23 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { function compileNodes(nodeList, transcludeFn, $rootElement, maxPriority, ignoreDirective, previousCompileContext) { var linkFns = [], + // `nodeList` can be either an element's `.childNodes` (live NodeList) + // or a jqLite/jQuery collection or an array + notLiveList = isArray(nodeList) || (nodeList instanceof jqLite), attrs, directives, nodeLinkFn, childNodes, childLinkFn, linkFnFound, nodeLinkFnFound; + for (var i = 0; i < nodeList.length; i++) { attrs = new Attributes(); - // we must always refer to nodeList[i] since the nodes can be replaced underneath us. + // Support: IE 11 only + // Workaround for #11781 and #14924 + if (msie === 11) { + mergeConsecutiveTextNodes(nodeList, i, notLiveList); + } + + // We must always refer to `nodeList[i]` hereafter, + // since the nodes can be replaced underneath us. directives = collectDirectives(nodeList[i], [], attrs, i === 0 ? maxPriority : undefined, ignoreDirective); @@ -8582,7 +9017,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { 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) { + for (i = 0; i < linkFns.length; i += 3) { idx = linkFns[i]; stableNodeList[idx] = nodeList[idx]; } @@ -8626,6 +9061,32 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } } + function mergeConsecutiveTextNodes(nodeList, idx, notLiveList) { + var node = nodeList[idx]; + var parent = node.parentNode; + var sibling; + + if (node.nodeType !== NODE_TYPE_TEXT) { + return; + } + + while (true) { + sibling = parent ? node.nextSibling : nodeList[idx + 1]; + if (!sibling || sibling.nodeType !== NODE_TYPE_TEXT) { + break; + } + + node.nodeValue = node.nodeValue + sibling.nodeValue; + + if (sibling.parentNode) { + sibling.parentNode.removeChild(sibling); + } + if (notLiveList && sibling === nodeList[idx + 1]) { + nodeList.splice(idx + 1, 1); + } + } + } + function createBoundTranscludeFn(scope, transcludeFn, previousBoundTranscludeFn) { function boundTranscludeFn(transcludedScope, cloneFn, controllers, futureParentElement, containingScope) { @@ -8669,13 +9130,17 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { var nodeType = node.nodeType, attrsMap = attrs.$attr, match, + nodeName, className; switch (nodeType) { case NODE_TYPE_ELEMENT: /* Element */ + + nodeName = nodeName_(node); + // use the node name: <directive> addDirective(directives, - directiveNormalize(nodeName_(node)), 'E', maxPriority, ignoreDirective); + directiveNormalize(nodeName), 'E', maxPriority, ignoreDirective); // iterate over the attributes for (var attr, name, nName, ngAttrName, value, isNgAttr, nAttrs = node.attributes, @@ -8685,11 +9150,12 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { attr = nAttrs[j]; name = attr.name; - value = trim(attr.value); + value = attr.value; // support ngAttr attribute binding ngAttrName = directiveNormalize(name); - if (isNgAttr = NG_ATTR_BINDING.test(ngAttrName)) { + isNgAttr = NG_ATTR_BINDING.test(ngAttrName); + if (isNgAttr) { name = name.replace(PREFIX_REGEXP, '') .substr(8).replace(/_(.)/g, function(match, letter) { return letter.toUpperCase(); @@ -8716,14 +9182,21 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { attrEndName); } + if (nodeName === 'input' && node.getAttribute('type') === 'hidden') { + // Hidden input elements can have strange behaviour when navigating back to the page + // This tells the browser not to try to cache and reinstate previous values + node.setAttribute('autocomplete', 'off'); + } + // use class as directive + if (!cssClassDirectivesEnabled) break; className = node.className; if (isObject(className)) { // Maybe SVGAnimatedString className = className.animVal; } if (isString(className) && className !== '') { - while (match = CLASS_DIRECTIVE_REGEXP.exec(className)) { + while ((match = CLASS_DIRECTIVE_REGEXP.exec(className))) { nName = directiveNormalize(match[2]); if (addDirective(directives, nName, 'C', maxPriority, ignoreDirective)) { attrs[nName] = trim(match[3]); @@ -8733,16 +9206,10 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } 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; } @@ -8770,7 +9237,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } /** - * Given a node with an directive-start it collects all of the siblings until it finds + * Given a node with a directive-start it collects all of the siblings until it finds * directive-end. * @param node * @param attrStart @@ -8784,10 +9251,10 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { do { if (!node) { throw $compileMinErr('uterdir', - "Unterminated attribute, found '{0}' but no matching '{1}' found.", + 'Unterminated attribute, found \'{0}\' but no matching \'{1}\' found.', attrStart, attrEnd); } - if (node.nodeType == NODE_TYPE_ELEMENT) { + if (node.nodeType === NODE_TYPE_ELEMENT) { if (node.hasAttribute(attrStart)) depth++; if (node.hasAttribute(attrEnd)) depth--; } @@ -8833,7 +9300,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (eager) { return compile($compileNodes, transcludeFn, maxPriority, ignoreDirective, previousCompileContext); } - return function lazyCompilation() { + return /** @this */ function lazyCompilation() { if (!compiled) { compiled = compile($compileNodes, transcludeFn, maxPriority, ignoreDirective, previousCompileContext); @@ -8909,7 +9376,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { break; // prevent further processing of directives } - if (directiveValue = directive.scope) { + directiveValue = directive.scope; + + if (directiveValue) { // skip the check for directives with async templates, we'll check the derived sync // directive when the template arrives @@ -8943,7 +9412,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { || (directive.transclude && !directive.$$tlb))) { var candidateDirective; - for (var scanningIndex = i + 1; candidateDirective = directives[scanningIndex++];) { + for (var scanningIndex = i + 1; (candidateDirective = directives[scanningIndex++]);) { if ((candidateDirective.transclude && !candidateDirective.$$tlb) || (candidateDirective.replace && (candidateDirective.templateUrl || candidateDirective.template))) { mightHaveMultipleTransclusionError = true; @@ -8955,14 +9424,15 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } if (!directive.templateUrl && directive.controller) { - directiveValue = directive.controller; controllerDirectives = controllerDirectives || createMap(); - assertNoDuplicate("'" + directiveName + "' controller", + assertNoDuplicate('\'' + directiveName + '\' controller', controllerDirectives[directiveName], directive, $compileNode); controllerDirectives[directiveName] = directive; } - if (directiveValue = directive.transclude) { + directiveValue = directive.transclude; + + if (directiveValue) { hasTranscludeDirective = true; // Special case ngIf and ngRepeat so that we don't complain about duplicate transclusion. @@ -8973,7 +9443,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { nonTlbTranscludeDirective = directive; } - if (directiveValue == 'element') { + if (directiveValue === 'element') { hasElementTranscludeDirective = true; terminalPriority = directive.priority; $template = $compileNode; @@ -9008,9 +9478,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { var slots = createMap(); - $template = jqLite(jqLiteClone(compileNode)).contents(); - - if (isObject(directiveValue)) { + if (!isObject(directiveValue)) { + $template = jqLite(jqLiteClone(compileNode)).contents(); + } else { // We have transclusion slots, // collect them up, compile them and store their transclusion functions @@ -9091,9 +9561,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } compileNode = $template[0]; - if ($template.length != 1 || compileNode.nodeType !== NODE_TYPE_ELEMENT) { + if ($template.length !== 1 || compileNode.nodeType !== NODE_TYPE_ELEMENT) { throw $compileMinErr('tplrt', - "Template for directive '{0}' must have exactly one root element. {1}", + 'Template for directive \'{0}\' must have exactly one root element. {1}', directiveName, ''); } @@ -9133,9 +9603,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { replaceDirective = directive; } - /* jshint -W021 */ + // eslint-disable-next-line no-func-assign nodeLinkFn = compileTemplateUrl(directives.splice(i, directives.length - i), $compileNode, - /* jshint +W021 */ templateAttrs, jqCollection, hasTranscludeDirective && childTranscludeFn, preLinkFns, postLinkFns, { controllerDirectives: controllerDirectives, newScopeDirective: (newScopeDirective !== directive) && newScopeDirective, @@ -9253,20 +9722,29 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { var controller = elementControllers[name]; var bindings = controllerDirective.$$bindings.bindToController; - if (controller.identifier && bindings) { - controller.bindingInfo = - initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective); - } else { - controller.bindingInfo = {}; - } + 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); - controller.bindingInfo.removeWatches && controller.bindingInfo.removeWatches(); + 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); } @@ -9327,7 +9805,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (newIsolateScopeDirective && (newIsolateScopeDirective.template || newIsolateScopeDirective.templateUrl === null)) { scopeToChild = isolateScope; } - childLinkFn && childLinkFn(scopeToChild, linkNode.childNodes, undefined, boundTranscludeFn); + if (childLinkFn) { + childLinkFn(scopeToChild, linkNode.childNodes, undefined, boundTranscludeFn); + } // POSTLINKING for (i = postLinkFns.length - 1; i >= 0; i--) { @@ -9414,7 +9894,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (!value && !optional) { throw $compileMinErr('ctreq', - "Controller '{0}', required by directive '{1}', can't be found!", + 'Controller \'{0}\', required by directive \'{1}\', can\'t be found!', name, directiveName); } } else if (isArray(require)) { @@ -9444,7 +9924,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { }; var controller = directive.controller; - if (controller == '@') { + if (controller === '@') { controller = attrs[directive.name]; } @@ -9493,24 +9973,22 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (hasDirectives.hasOwnProperty(name)) { for (var directive, directives = $injector.get(name + Suffix), i = 0, ii = directives.length; i < ii; i++) { - try { - 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; - } + 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; } - } catch (e) { $exceptionHandler(e); } + tDirectives.push(directive); + match = directive; + } } } return match; @@ -9548,14 +10026,17 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { */ function mergeTemplateAttributes(dst, src) { var srcAttr = src.$attr, - dstAttr = dst.$attr, - $element = dst.$$element; + dstAttr = dst.$attr; // reapply the old attributes to the new element forEach(dst, function(value, key) { - if (key.charAt(0) != '$') { + if (key.charAt(0) !== '$') { if (src[key] && src[key] !== value) { - value += (key === 'style' ? ';' : ' ') + src[key]; + if (value.length) { + value += (key === 'style' ? ';' : ' ') + src[key]; + } else { + value = src[key]; + } } dst.$set(key, value, true, srcAttr[key]); } @@ -9609,9 +10090,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } compileNode = $template[0]; - if ($template.length != 1 || compileNode.nodeType !== NODE_TYPE_ELEMENT) { + if ($template.length !== 1 || compileNode.nodeType !== NODE_TYPE_ELEMENT) { throw $compileMinErr('tplrt', - "Template for directive '{0}' must have exactly one root element. {1}", + 'Template for directive \'{0}\' must have exactly one root element. {1}', origAsyncDirective.name, templateUrl); } @@ -9637,7 +10118,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { childTranscludeFn, $compileNode, origAsyncDirective, preLinkFns, postLinkFns, previousCompileContext); forEach($rootElement, function(node, i) { - if (node == compileNode) { + if (node === compileNode) { $rootElement[i] = $compileNode[0]; } }); @@ -9674,6 +10155,10 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { childBoundTranscludeFn); } linkQueue = null; + }).catch(function(error) { + if (error instanceof Error) { + $exceptionHandler(error); + } }); return function delayedNodeLinkFn(ignoreChildLinkFn, scope, node, rootElement, boundTranscludeFn) { @@ -9762,36 +10247,49 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { function getTrustedContext(node, attrNormalizedName) { - if (attrNormalizedName == "srcdoc") { + 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 <maction>. - if (attrNormalizedName == "xlinkHref" || - (tag == "form" && attrNormalizedName == "action") || - (tag != "img" && (attrNormalizedName == "src" || - attrNormalizedName == "ngSrc"))) { + } else if (attrNormalizedName === 'xlinkHref' || + (tag === 'form' && attrNormalizedName === 'action') || + // links can be stylesheets or imports, which can run script in the current origin + (tag === 'link' && attrNormalizedName === 'href') + ) { return $sce.RESOURCE_URL; } } - function addAttrInterpolateDirective(node, directives, value, name, allOrNothing) { + function addAttrInterpolateDirective(node, directives, value, name, isNgAttr) { var trustedContext = getTrustedContext(node, name); - allOrNothing = ALL_OR_NOTHING_ATTRS[name] || allOrNothing; + var mustHaveExpression = !isNgAttr; + var allOrNothing = ALL_OR_NOTHING_ATTRS[name] || isNgAttr; - var interpolateFn = $interpolate(value, true, trustedContext, allOrNothing); + 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}", + if (name === 'multiple' && nodeName_(node) === 'select') { + throw $compileMinErr('selmulti', + 'Binding to the \'multiple\' attribute is not supported. Element: {0}', startingTag(node)); } + if (EVENT_HANDLER_ATTR_REGEXP.test(name)) { + throw $compileMinErr('nodomevents', + 'Interpolations for HTML DOM event attributes are disallowed. Please use the ' + + 'ng- versions (such as ng-click instead of onclick) instead.'); + } + directives.push({ priority: 100, compile: function() { @@ -9799,12 +10297,6 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { 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) { @@ -9833,7 +10325,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { //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) { + if (name === 'class' && newValue !== oldValue) { attr.$updateClass(newValue, oldValue); } else { attr.$set(name, newValue); @@ -9864,7 +10356,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if ($rootElement) { for (i = 0, ii = $rootElement.length; i < ii; i++) { - if ($rootElement[i] == firstElementToRemove) { + if ($rootElement[i] === firstElementToRemove) { $rootElement[i++] = newNode; for (var j = i, j2 = j + removeCount - 1, jj = $rootElement.length; @@ -9938,8 +10430,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } - // Set up $watches for isolate scope and controller bindings. This process - // only occurs for isolate scopes and new scopes with controllerAs. + // Set up $watches for isolate scope and controller bindings. function initializeDirectiveBindings(scope, attrs, destination, bindings, directive) { var removeWatchCollection = []; var initialChanges = {}; @@ -9955,9 +10446,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { case '@': if (!optional && !hasOwnProperty.call(attrs, attrName)) { - destination[scopeName] = attrs[attrName] = void 0; + destination[scopeName] = attrs[attrName] = undefined; } - attrs.$observe(attrName, function(value) { + removeWatch = attrs.$observe(attrName, function(value) { if (isString(value) || isBoolean(value)) { var oldValue = destination[scopeName]; recordChanges(scopeName, value, oldValue); @@ -9976,12 +10467,13 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { 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] = void 0; + attrs[attrName] = undefined; } if (optional && !attrs[attrName]) break; @@ -9989,13 +10481,13 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (parentGet.literal) { compare = equals; } else { - compare = function simpleCompare(a, b) { return a === b || (a !== a && b !== b); }; + compare = simpleCompare; } parentSet = parentGet.assign || function() { // reset the change, or we will throw this exception on every $digest lastValue = destination[scopeName] = parentGet(scope); throw $compileMinErr('nonassign', - "Expression '{0}' in attribute '{1}' used with directive '{2}' is non-assignable!", + 'Expression \'{0}\' in attribute \'{1}\' used with directive \'{2}\' is non-assignable!', attrs[attrName], attrName, directive.name); }; lastValue = destination[scopeName] = parentGet(scope); @@ -10010,7 +10502,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { parentSet(scope, parentValue = destination[scopeName]); } } - return lastValue = parentValue; + lastValue = parentValue; + return lastValue; }; parentValueWatch.$stateful = true; if (definition.collection) { @@ -10024,23 +10517,26 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { case '<': if (!hasOwnProperty.call(attrs, attrName)) { if (optional) break; - attrs[attrName] = void 0; + 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) return; + if (oldValue === initialValue || (deepWatch && equals(oldValue, initialValue))) { + return; + } oldValue = initialValue; } recordChanges(scopeName, newValue, oldValue); destination[scopeName] = newValue; - }, parentGet.literal); + }, deepWatch); removeWatchCollection.push(removeWatch); break; @@ -10060,7 +10556,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { }); function recordChanges(key, currentValue, previousValue) { - if (isFunction(destination.$onChanges) && currentValue !== previousValue) { + if (isFunction(destination.$onChanges) && !simpleCompare(currentValue, previousValue)) { // If we have not already scheduled the top level onChangesQueue handler then do so now if (!onChangesQueue) { scope.$$postDigest(flushOnChangesQueue); @@ -10105,13 +10601,17 @@ function SimpleChange(previous, current) { SimpleChange.prototype.isFirstChange = function() { return this.previousValue === _UNINITIALIZED_VALUE; }; -var PREFIX_REGEXP = /^((?:x|data)[\:\-_])/i; +var PREFIX_REGEXP = /^((?:x|data)[:\-_])/i; +var SPECIAL_CHARS_REGEXP = /[:\-_]+(.)/g; + /** * Converts all accepted directives format into proper directive name. * @param name Name to normalize */ function directiveNormalize(name) { - return camelCase(name.replace(PREFIX_REGEXP, '')); + return name + .replace(PREFIX_REGEXP, '') + .replace(SPECIAL_CHARS_REGEXP, fnCamelCaseReplace); } /** @@ -10183,7 +10683,7 @@ function tokenDifference(str1, str2) { 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; + if (token === tokens2[j]) continue outer; } values += (values.length > 0 ? ' ' : '') + token; } @@ -10200,8 +10700,9 @@ function removeComments(jqNodes) { while (i--) { var node = jqNodes[i]; - if (node.nodeType === NODE_TYPE_COMMENT) { - splice.call(jqNodes, i, 1); + if (node.nodeType === NODE_TYPE_COMMENT || + (node.nodeType === NODE_TYPE_TEXT && node.nodeValue.trim() === '')) { + splice.call(jqNodes, i, 1); } } return jqNodes; @@ -10223,6 +10724,8 @@ function identifierForController(controller, ident) { /** * @ngdoc provider * @name $controllerProvider + * @this + * * @description * The {@link ng.$controller $controller service} is used by Angular to create new * controllers. @@ -10264,6 +10767,11 @@ function $ControllerProvider() { * @ngdoc method * @name $controllerProvider#allowGlobals * @description If called, allows `$controller` to find controller constructors on `window` + * + * @deprecated + * sinceVersion="v1.3.0" + * removeVersion="v1.7.0" + * This method of finding controllers has been deprecated. */ this.allowGlobals = function() { globals = true; @@ -10284,7 +10792,7 @@ function $ControllerProvider() { * * 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) + * `window` object (deprecated, not recommended) * * The string can use the `controller as property` syntax, where the controller instance is published * as the specified property on the `scope`; the `scope` must be injected into `locals` param for this @@ -10317,16 +10825,21 @@ function $ControllerProvider() { match = expression.match(CNTRL_REG); if (!match) { throw $controllerMinErr('ctrlfmt', - "Badly formed controller string '{0}'. " + - "Must match `__name__ as __id__` or `__name__`.", expression); + 'Badly formed controller string \'{0}\'. ' + + 'Must match `__name__ as __id__` or `__name__`.', expression); } - constructor = match[1], + 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); } @@ -10349,8 +10862,7 @@ function $ControllerProvider() { addIdentifier(locals, identifier, instance, constructor || expression.name); } - var instantiate; - return instantiate = extend(function $controllerInit() { + return extend(function $controllerInit() { var result = $injector.invoke(expression, instance, locals, constructor); if (result !== instance && (isObject(result) || isFunction(result))) { instance = result; @@ -10378,7 +10890,7 @@ function $ControllerProvider() { 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`.", + 'Cannot export controller \'{0}\' as \'{1}\'! No $scope object provided via `locals`.', name, identifier); } @@ -10391,12 +10903,13 @@ function $ControllerProvider() { * @ngdoc service * @name $document * @requires $window + * @this * * @description * A {@link angular.element jQuery or jqLite} wrapper for the browser's `window.document` object. * * @example - <example module="documentExample"> + <example module="documentExample" name="document"> <file name="index.html"> <div ng-controller="ExampleController"> <p>$document title: <b ng-bind="title"></b></p> @@ -10418,10 +10931,38 @@ function $DocumentProvider() { }]; } + +/** + * @private + * @this + * Listens for document visibility change and makes the current status accessible. + */ +function $$IsDocumentHiddenProvider() { + this.$get = ['$document', '$rootScope', function($document, $rootScope) { + var doc = $document[0]; + var hidden = doc && doc.hidden; + + $document.on('visibilitychange', changeListener); + + $rootScope.$on('$destroy', function() { + $document.off('visibilitychange', changeListener); + }); + + function changeListener() { + hidden = doc.hidden; + } + + return function() { + return hidden; + }; + }]; +} + /** * @ngdoc service * @name $exceptionHandler * @requires ng.$log + * @this * * @description * Any uncaught exception in angular expressions is delegated to this service. @@ -10469,7 +11010,7 @@ function $ExceptionHandlerProvider() { }]; } -var $$ForceReflowProvider = function() { +var $$ForceReflowProvider = /** @this */ function() { this.$get = ['$document', function($document) { return function(domNode) { //the line below will force the browser to perform a repaint so @@ -10499,13 +11040,8 @@ var JSON_ENDS = { '[': /]$/, '{': /}$/ }; -var JSON_PROTECTION_PREFIX = /^\)\]\}',?\n/; +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)) { @@ -10515,6 +11051,7 @@ function serializeValue(v) { } +/** @this */ function $HttpParamSerializerProvider() { /** * @ngdoc service @@ -10552,10 +11089,12 @@ function $HttpParamSerializerProvider() { }; } +/** @this */ function $HttpParamSerializerJQLikeProvider() { /** * @ngdoc service * @name $httpParamSerializerJQLike + * * @description * * Alternative {@link $http `$http`} params serializer that follows @@ -10632,7 +11171,12 @@ function defaultHttpResponseTransform(data, headers) { if (tempData) { var contentType = headers('Content-Type'); if ((contentType && (contentType.indexOf(APPLICATION_JSON) === 0)) || isJsonLike(tempData)) { - data = fromJson(tempData); + try { + data = fromJson(tempData); + } catch (e) { + throw $httpMinErr('baddata', 'Data must be a valid JSON object. Received: "{0}". ' + + 'Parse error: "{1}"', data, e); + } } } } @@ -10684,7 +11228,7 @@ function parseHeaders(headers) { * @param {(string|Object)} headers Headers to provide access to. * @returns {function(string=)} Returns a getter function which if called with: * - * - if called with single an argument returns a single header value or null + * - if called with an argument returns a single header value or null * - if called with no arguments returns an object containing all headers. */ function headersGetter(headers) { @@ -10695,7 +11239,7 @@ function headersGetter(headers) { if (name) { var value = headersObj[lowercase(name)]; - if (value === void 0) { + if (value === undefined) { value = null; } return value; @@ -10738,6 +11282,8 @@ function isSuccess(status) { /** * @ngdoc provider * @name $httpProvider + * @this + * * @description * Use `$httpProvider` to change the default behavior of the {@link ng.$http $http} service. * */ @@ -10773,6 +11319,10 @@ function $HttpProvider() { * If specified as string, it is interpreted as a function registered with the {@link auto.$injector $injector}. * Defaults to {@link ng.$httpParamSerializer $httpParamSerializer}. * + * - **`defaults.jsonpCallbackParam`** - `{string}` - the name of the query parameter that passes the name of the + * callback in a JSONP request. The value of this parameter will be replaced with the expression generated by the + * {@link $jsonpCallbacks} service. Defaults to `'callback'`. + * **/ var defaults = this.defaults = { // transform incoming response data @@ -10796,7 +11346,9 @@ function $HttpProvider() { xsrfCookieName: 'XSRF-TOKEN', xsrfHeaderName: 'X-XSRF-TOKEN', - paramSerializer: '$httpParamSerializer' + paramSerializer: '$httpParamSerializer', + + jsonpCallbackParam: 'callback' }; var useApplyAsync = false; @@ -10827,30 +11379,6 @@ function $HttpProvider() { return useApplyAsync; }; - var useLegacyPromise = true; - /** - * @ngdoc method - * @name $httpProvider#useLegacyPromiseExtensions - * @description - * - * 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 @@ -10866,8 +11394,8 @@ function $HttpProvider() { **/ var interceptorFactories = this.interceptors = []; - this.$get = ['$httpBackend', '$$cookieReader', '$cacheFactory', '$rootScope', '$q', '$injector', - function($httpBackend, $$cookieReader, $cacheFactory, $rootScope, $q, $injector) { + this.$get = ['$browser', '$httpBackend', '$$cookieReader', '$cacheFactory', '$rootScope', '$q', '$injector', '$sce', + function($browser, $httpBackend, $$cookieReader, $cacheFactory, $rootScope, $q, $injector, $sce) { var defaultCache = $cacheFactory('$http'); @@ -10984,14 +11512,6 @@ function $HttpProvider() { * $httpBackend.flush(); * ``` * - * ## Deprecation Notice - * <div class="alert alert-danger"> - * The `$http` legacy promise methods `success` and `error` have been deprecated. - * 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. - * </div> - * * ## Setting HTTP Headers * * The $http service will automatically add certain HTTP headers to all requests. These defaults @@ -10999,7 +11519,7 @@ function $HttpProvider() { * object, which currently contains this default configuration: * * - `$httpProvider.defaults.headers.common` (headers that are common for all requests): - * - `Accept: application/json, text/plain, * / *` + * - <code>Accept: application/json, text/plain, \*/\*</code> * - `$httpProvider.defaults.headers.post`: (header defaults for POST requests) * - `Content-Type: application/json` * - `$httpProvider.defaults.headers.put` (header defaults for PUT requests) @@ -11289,7 +11809,8 @@ function $HttpProvider() { * 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. + * - **url** – `{string|TrustedObject}` – Absolute or relative URL of the resource that is being requested; + * or an object created by a call to `$sce.trustAsResourceUrl(url)`. * - **params** – `{Object.<string|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. @@ -11343,7 +11864,7 @@ function $HttpProvider() { * * * @example -<example module="httpExample"> +<example module="httpExample" name="http-service"> <file name="index.html"> <div ng-controller="FetchController"> <select ng-model="method" aria-label="Request method"> @@ -11355,11 +11876,11 @@ function $HttpProvider() { <button id="samplegetbtn" ng-click="updateModel('GET', 'http-hello.html')">Sample GET</button> <button id="samplejsonpbtn" ng-click="updateModel('JSONP', - 'https://angularjs.org/greet.php?callback=JSON_CALLBACK&name=Super%20Hero')"> + 'https://angularjs.org/greet.php?name=Super%20Hero')"> Sample JSONP </button> <button id="invalidjsonpbtn" - ng-click="updateModel('JSONP', 'https://angularjs.org/doesntexist&callback=JSON_CALLBACK')"> + ng-click="updateModel('JSONP', 'https://angularjs.org/doesntexist')"> Invalid JSONP </button> <pre>http status code: {{status}}</pre> @@ -11368,6 +11889,13 @@ function $HttpProvider() { </file> <file name="script.js"> angular.module('httpExample', []) + .config(['$sceDelegateProvider', function($sceDelegateProvider) { + // We must whitelist the JSONP endpoint that we are using to show that we trust it + $sceDelegateProvider.resourceUrlWhitelist([ + 'self', + 'https://angularjs.org/**' + ]); + }]) .controller('FetchController', ['$scope', '$http', '$templateCache', function($scope, $http, $templateCache) { $scope.method = 'GET'; @@ -11382,7 +11910,7 @@ function $HttpProvider() { $scope.status = response.status; $scope.data = response.data; }, function(response) { - $scope.data = response.data || "Request failed"; + $scope.data = response.data || 'Request failed'; $scope.status = response.status; }); }; @@ -11401,7 +11929,6 @@ function $HttpProvider() { var data = element(by.binding('data')); var fetchBtn = element(by.id('fetchbtn')); var sampleGetBtn = element(by.id('samplegetbtn')); - var sampleJsonpBtn = element(by.id('samplejsonpbtn')); var invalidJsonpBtn = element(by.id('invalidjsonpbtn')); it('should make an xhr GET request', function() { @@ -11413,6 +11940,7 @@ function $HttpProvider() { // 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'); @@ -11435,15 +11963,16 @@ function $HttpProvider() { 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); + if (!isString($sce.valueOf(requestConfig.url))) { + throw minErr('$http')('badreq', 'Http request configuration url must be a string or a $sce trusted object. Received: {0}', requestConfig.url); } var config = extend({ method: 'get', transformRequest: defaults.transformRequest, transformResponse: defaults.transformResponse, - paramSerializer: defaults.paramSerializer + paramSerializer: defaults.paramSerializer, + jsonpCallbackParam: defaults.jsonpCallbackParam }, requestConfig); config.headers = mergeHeaders(requestConfig); @@ -11451,9 +11980,11 @@ function $HttpProvider() { config.paramSerializer = isString(config.paramSerializer) ? $injector.get(config.paramSerializer) : config.paramSerializer; + $browser.$$incOutstandingRequestCount(); + var requestInterceptors = []; var responseInterceptors = []; - var promise = $q.when(config); + var promise = $q.resolve(config); // apply interceptors forEach(reversedInterceptors, function(interceptor) { @@ -11468,29 +11999,7 @@ function $HttpProvider() { 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'); - } + promise = promise.finally(completeOutstandingRequest); return promise; @@ -11508,6 +12017,10 @@ function $HttpProvider() { return promise; } + function completeOutstandingRequest() { + $browser.$$completeOutstandingRequest(noop); + } + function executeHeaderFns(headers, config) { var headerContent, processedHeaders = {}; @@ -11591,7 +12104,8 @@ function $HttpProvider() { * @description * Shortcut method to perform `GET` request. * - * @param {string} url Relative or absolute URL specifying the destination of the request + * @param {string|TrustedObject} url Absolute or relative URL of the resource that is being requested; + * or an object created by a call to `$sce.trustAsResourceUrl(url)`. * @param {Object=} config Optional configuration object * @returns {HttpPromise} Future object */ @@ -11603,7 +12117,8 @@ function $HttpProvider() { * @description * Shortcut method to perform `DELETE` request. * - * @param {string} url Relative or absolute URL specifying the destination of the request + * @param {string|TrustedObject} url Absolute or relative URL of the resource that is being requested; + * or an object created by a call to `$sce.trustAsResourceUrl(url)`. * @param {Object=} config Optional configuration object * @returns {HttpPromise} Future object */ @@ -11615,7 +12130,8 @@ function $HttpProvider() { * @description * Shortcut method to perform `HEAD` request. * - * @param {string} url Relative or absolute URL specifying the destination of the request + * @param {string|TrustedObject} url Absolute or relative URL of the resource that is being requested; + * or an object created by a call to `$sce.trustAsResourceUrl(url)`. * @param {Object=} config Optional configuration object * @returns {HttpPromise} Future object */ @@ -11626,11 +12142,34 @@ function $HttpProvider() { * * @description * Shortcut method to perform `JSONP` request. + * + * Note that, since JSONP requests are sensitive because the response is given full access to the browser, + * the url must be declared, via {@link $sce} as a trusted resource URL. + * You can trust a URL by adding it to the whitelist via + * {@link $sceDelegateProvider#resourceUrlWhitelist `$sceDelegateProvider.resourceUrlWhitelist`} or + * by explicitly trusting the URL via {@link $sce#trustAsResourceUrl `$sce.trustAsResourceUrl(url)`}. + * + * JSONP requests must specify a callback to be used in the response from the server. This callback + * is passed as a query parameter in the request. You must specify the name of this parameter by + * setting the `jsonpCallbackParam` property on the request config object. + * + * ``` + * $http.jsonp('some/trusted/url', {jsonpCallbackParam: 'callback'}) + * ``` + * + * You can also specify a default callback parameter name in `$http.defaults.jsonpCallbackParam`. + * Initially this is set to `'callback'`. + * + * <div class="alert alert-danger"> + * You can no longer use the `JSON_CALLBACK` string as a placeholder for specifying where the callback + * parameter value should go. + * </div> + * * If you would like to customise 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 {string|TrustedObject} url Absolute or relative URL of the resource that is being requested; + * or an object created by a call to `$sce.trustAsResourceUrl(url)`. * @param {Object=} config Optional configuration object * @returns {HttpPromise} Future object */ @@ -11729,16 +12268,33 @@ function $HttpProvider() { cache, cachedResp, reqHeaders = config.headers, - url = buildUrl(config.url, config.paramSerializer(config.params)); + isJsonp = lowercase(config.method) === 'jsonp', + url = config.url; + + if (isJsonp) { + // JSONP is a pretty sensitive operation where we're allowing a script to have full access to + // our DOM and JS space. So we require that the URL satisfies SCE.RESOURCE_URL. + url = $sce.getTrustedResourceUrl(url); + } else if (!isString(url)) { + // If it is not a string then the URL must be a $sce trusted object + url = $sce.valueOf(url); + } + + url = buildUrl(url, config.paramSerializer(config.params)); + + if (isJsonp) { + // Check the url and add the JSONP callback placeholder + url = sanitizeJsonpCallbackParam(url, config.jsonpCallbackParam); + } $http.pendingRequests.push(config); promise.then(removePendingReq, removePendingReq); - if ((config.cache || defaults.cache) && config.cache !== false && (config.method === 'GET' || config.method === 'JSONP')) { cache = isObject(config.cache) ? config.cache - : isObject(defaults.cache) ? defaults.cache + : isObject(/** @type {?} */ (defaults).cache) + ? /** @type {?} */ (defaults).cache : defaultCache; } @@ -11862,16 +12418,35 @@ function $HttpProvider() { function buildUrl(url, serializedParams) { if (serializedParams.length > 0) { - url += ((url.indexOf('?') == -1) ? '?' : '&') + serializedParams; + url += ((url.indexOf('?') === -1) ? '?' : '&') + serializedParams; } return url; } + + function sanitizeJsonpCallbackParam(url, key) { + if (/[&?][^=]+=JSON_CALLBACK/.test(url)) { + // Throw if the url already contains a reference to JSON_CALLBACK + throw $httpMinErr('badjsonp', 'Illegal use of JSON_CALLBACK in url, "{0}"', url); + } + + var callbackParamRegex = new RegExp('[&?]' + key + '='); + if (callbackParamRegex.test(url)) { + // Throw if the callback param was already provided + throw $httpMinErr('badjsonp', 'Illegal use of callback param, "{0}", in url, "{1}"', key, url); + } + + // Add in the JSON_CALLBACK callback param value + url += ((url.indexOf('?') === -1) ? '?' : '&') + key + '=JSON_CALLBACK'; + + return url; + } }]; } /** * @ngdoc service * @name $xhrFactory + * @this * * @description * Factory function used to create XMLHttpRequest objects. @@ -11904,6 +12479,7 @@ function $xhrFactoryProvider() { * @requires $jsonpCallbacks * @requires $document * @requires $xhrFactory + * @this * * @description * HTTP backend used by the {@link ng.$http service} that delegates to @@ -11924,7 +12500,6 @@ function $HttpBackendProvider() { 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') { @@ -11932,7 +12507,7 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc 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); + completeRequest(callback, status, response, '', text); callbacks.removeCallback(callbackPath); }); } else { @@ -11960,7 +12535,7 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc // 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; + status = response ? 200 : urlResolve(url).protocol === 'file' ? 404 : 0; } completeRequest(callback, @@ -11978,6 +12553,7 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc xhr.onerror = requestError; xhr.onabort = requestError; + xhr.ontimeout = requestError; forEach(eventHandlers, function(value, key) { xhr.addEventListener(key, value); @@ -12019,8 +12595,12 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc function timeoutRequest() { - jsonpDone && jsonpDone(); - xhr && xhr.abort(); + if (jsonpDone) { + jsonpDone(); + } + if (xhr) { + xhr.abort(); + } } function completeRequest(callback, status, response, headersString, statusText) { @@ -12031,7 +12611,6 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc jsonpDone = xhr = null; callback(status, response, headersString, statusText); - $browser.$$completeOutstandingRequest(noop); } }; @@ -12041,24 +12620,24 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc // - 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.type = 'text/javascript'; script.src = url; script.async = true; callback = function(event) { - removeEventListenerFn(script, "load", callback); - removeEventListenerFn(script, "error", callback); + script.removeEventListener('load', callback); + script.removeEventListener('error', callback); rawDocument.body.removeChild(script); script = null; var status = -1; - var text = "unknown"; + var text = 'unknown'; if (event) { - if (event.type === "load" && !callbacks.wasCalled(callbackPath)) { - event = { type: "error" }; + if (event.type === 'load' && !callbacks.wasCalled(callbackPath)) { + event = { type: 'error' }; } text = event.type; - status = event.type === "error" ? 404 : 200; + status = event.type === 'error' ? 404 : 200; } if (done) { @@ -12066,8 +12645,8 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc } }; - addEventListenerFn(script, "load", callback); - addEventListenerFn(script, "error", callback); + script.addEventListener('load', callback); + script.addEventListener('error', callback); rawDocument.body.appendChild(script); return callback; } @@ -12076,18 +12655,19 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc 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); + '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()); + return $interpolateMinErr('interr', 'Can\'t interpolate: {0}\n{1}', text, err.toString()); }; /** * @ngdoc provider * @name $interpolateProvider + * @this * * @description * @@ -12184,30 +12764,13 @@ function $InterpolateProvider() { 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 + // TODO: this is the same as the constantWatchDelegate in parse.js function constantWatchDelegate(scope, listener, objectEquality, constantInterp) { - var unwatch; - return unwatch = scope.$watch(function constantInterpolateWatch(scope) { + var unwatch = scope.$watch(function constantInterpolateWatch(scope) { unwatch(); return constantInterp(scope); }, listener, objectEquality); + return unwatch; } /** @@ -12274,7 +12837,7 @@ function $InterpolateProvider() { * 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. * - * <example> + * <example name="interpolation"> * <file name="index.html"> * <div ng-init="username='A user'"> * <p ng-init="apptitle='Escaping demo'">{{apptitle}}: \{\{ username = "defaced value"; \}\} @@ -12354,8 +12917,8 @@ function $InterpolateProvider() { expressionPositions = []; while (index < textLength) { - if (((startIndex = text.indexOf(startSymbol, index)) != -1) && - ((endIndex = text.indexOf(endSymbol, startIndex + startSymbolLength)) != -1)) { + if (((startIndex = text.indexOf(startSymbol, index)) !== -1) && + ((endIndex = text.indexOf(endSymbol, startIndex + startSymbolLength)) !== -1)) { if (index !== startIndex) { concat.push(unescapeText(text.substring(index, startIndex))); } @@ -12420,7 +12983,7 @@ function $InterpolateProvider() { expressions: expressions, $$watchDelegate: function(scope, listener) { var lastValue; - return scope.$watchGroup(parseFns, function interpolateFnWatcher(values, oldValues) { + 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); @@ -12477,6 +13040,7 @@ function $InterpolateProvider() { }]; } +/** @this */ function $IntervalProvider() { this.$get = ['$rootScope', '$window', '$q', '$$q', '$browser', function($rootScope, $window, $q, $$q, $browser) { @@ -12509,17 +13073,18 @@ function $IntervalProvider() { * appropriate moment. See the example below for more details on how and when to do this. * </div> * - * @param {function()} fn A function that should be called repeatedly. + * @param {function()} fn A function that should be called repeatedly. If no additional arguments + * are passed (see below), the function is called with the current iteration count. * @param {number} delay Number of milliseconds between each function call. * @param {number=} [count=0] Number of times to repeat. If not set, or 0, will repeat * indefinitely. * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block. * @param {...*=} Pass additional parameters to the executed function. - * @returns {promise} A promise which will be notified on each iteration. + * @returns {promise} A promise which will be notified on each iteration. It will resolve once all iterations of the interval complete. * * @example - * <example module="intervalExample"> + * <example module="intervalExample" name="interval-service"> * <file name="index.html"> * <script> * angular.module('intervalExample', []) @@ -12664,6 +13229,8 @@ function $IntervalProvider() { */ interval.cancel = function(promise) { if (promise && promise.$$intervalId in intervals) { + // Interval cancels should not report as unhandled promise. + intervals[promise.$$intervalId].promise.catch(noop); intervals[promise.$$intervalId].reject('canceled'); $window.clearInterval(promise.$$intervalId); delete intervals[promise.$$intervalId]; @@ -12685,9 +13252,9 @@ function $IntervalProvider() { * Override this service if you wish to customise where the callbacks are stored and * how they vary compared to the requested url. */ -var $jsonpCallbacksProvider = function() { - this.$get = ['$window', function($window) { - var callbacks = $window.angular.callbacks; +var $jsonpCallbacksProvider = /** @this */ function() { + this.$get = function() { + var callbacks = angular.callbacks; var callbackMap = {}; function createCallback(callbackId) { @@ -12754,7 +13321,7 @@ var $jsonpCallbacksProvider = function() { delete callbackMap[callbackPath]; } }; - }]; + }; }; /** @@ -12768,7 +13335,7 @@ var $jsonpCallbacksProvider = function() { * * `id` – `{string}` – locale id formatted as `languageId-countryId` (e.g. `en-us`) */ -var PATH_MATCH = /^([^\?#]*)(\?([^#]*))?(#(.*))?$/, +var PATH_MATCH = /^([^?#]*)(\?([^#]*))?(#(.*))?$/, DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp': 21}; var $locationMinErr = minErr('$location'); @@ -12798,26 +13365,31 @@ function parseAbsoluteUrl(absoluteUrl, locationObj) { locationObj.$$port = toInt(parsedUrl.port) || DEFAULT_PORTS[parsedUrl.protocol] || null; } +var DOUBLE_SLASH_REGEX = /^\s*[\\/]{2,}/; +function parseAppUrl(url, locationObj) { -function parseAppUrl(relativeUrl, locationObj) { - var prefixed = (relativeUrl.charAt(0) !== '/'); + if (DOUBLE_SLASH_REGEX.test(url)) { + throw $locationMinErr('badpath', 'Invalid url "{0}".', url); + } + + var prefixed = (url.charAt(0) !== '/'); if (prefixed) { - relativeUrl = '/' + relativeUrl; + url = '/' + url; } - var match = urlResolve(relativeUrl); + 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) != '/') { + if (locationObj.$$path && locationObj.$$path.charAt(0) !== '/') { locationObj.$$path = '/' + locationObj.$$path; } } -function startsWith(haystack, needle) { - return haystack.lastIndexOf(needle, 0) === 0; +function startsWith(str, search) { + return str.slice(0, search.length) === search; } /** @@ -12836,7 +13408,7 @@ function stripBaseUrl(base, url) { function stripHash(url) { var index = url.indexOf('#'); - return index == -1 ? url : url.substr(0, index); + return index === -1 ? url : url.substr(0, index); } function trimEmptyHash(url) { @@ -12855,13 +13427,13 @@ function serverBase(url) { /** - * LocationHtml5Url represents an url + * LocationHtml5Url represents a URL * This object is exposed as $location service when HTML5 mode is enabled and supported * * @constructor * @param {string} appBase application base URL * @param {string} appBaseNoFile application base URL stripped of any filename - * @param {string} basePrefix url path prefix + * @param {string} basePrefix URL path prefix */ function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) { this.$$html5 = true; @@ -12870,8 +13442,8 @@ function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) { /** - * Parse given html5 (regular) url string into properties - * @param {string} url HTML5 url + * Parse given HTML5 (regular) URL string into properties + * @param {string} url HTML5 URL * @private */ this.$$parse = function(url) { @@ -12900,6 +13472,8 @@ function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) { this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; this.$$absUrl = appBaseNoFile + this.$$url.substr(1); // first char is always '/' + + this.$$urlUpdatedByLocation = true; }; this.$$parseLinkUrl = function(url, relHref) { @@ -12912,16 +13486,17 @@ function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) { var appUrl, prevAppUrl; var rewrittenUrl; + if (isDefined(appUrl = stripBaseUrl(appBase, url))) { prevAppUrl = appUrl; - if (isDefined(appUrl = stripBaseUrl(basePrefix, 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 + '/') { + } else if (appBaseNoFile === url + '/') { rewrittenUrl = appBaseNoFile; } if (rewrittenUrl) { @@ -12933,7 +13508,7 @@ function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) { /** - * LocationHashbangUrl represents url + * LocationHashbangUrl represents URL * This object is exposed as $location service when developer doesn't opt into html5 mode. * It also serves as the base class for html5 mode fallback on legacy browsers. * @@ -12948,8 +13523,8 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) { /** - * Parse given hashbang url into properties - * @param {string} url Hashbang url + * Parse given hashbang URL into properties + * @param {string} url Hashbang URL * @private */ this.$$parse = function(url) { @@ -12958,7 +13533,7 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) { if (!isUndefined(withoutBaseUrl) && withoutBaseUrl.charAt(0) === '#') { - // The rest of the url starts with a hash so we have + // 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)) { @@ -12976,7 +13551,7 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) { withoutHashUrl = ''; if (isUndefined(withoutBaseUrl)) { appBase = url; - this.replace(); + /** @type {?} */ (this).replace(); } } } @@ -13023,7 +13598,7 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) { }; /** - * Compose hashbang url and update `absUrl` property + * Compose hashbang URL and update `absUrl` property * @private */ this.$$compose = function() { @@ -13032,10 +13607,12 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) { this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; this.$$absUrl = appBase + (this.$$url ? hashPrefix + this.$$url : ''); + + this.$$urlUpdatedByLocation = true; }; this.$$parseLinkUrl = function(url, relHref) { - if (stripHash(appBase) == stripHash(url)) { + if (stripHash(appBase) === stripHash(url)) { this.$$parse(url); return true; } @@ -13045,7 +13622,7 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) { /** - * LocationHashbangUrl represents url + * LocationHashbangUrl represents URL * This object is exposed as $location service when html5 history api is enabled but the browser * does not support it. * @@ -13069,7 +13646,7 @@ function LocationHashbangInHtml5Url(appBase, appBaseNoFile, hashPrefix) { var rewrittenUrl; var appUrl; - if (appBase == stripHash(url)) { + if (appBase === stripHash(url)) { rewrittenUrl = url; } else if ((appUrl = stripBaseUrl(appBaseNoFile, url))) { rewrittenUrl = appBase + hashPrefix + appUrl; @@ -13089,6 +13666,8 @@ function LocationHashbangInHtml5Url(appBase, appBaseNoFile, hashPrefix) { this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; // include hashPrefix in $$absUrl when $$url is empty so IE9 does not reload page because of removal of '#' this.$$absUrl = appBase + hashPrefix + this.$$url; + + this.$$urlUpdatedByLocation = true; }; } @@ -13097,7 +13676,7 @@ function LocationHashbangInHtml5Url(appBase, appBaseNoFile, hashPrefix) { var locationPrototype = { /** - * Ensure absolute url is initialized. + * Ensure absolute URL is initialized. * @private */ $$absUrl:'', @@ -13121,17 +13700,17 @@ var locationPrototype = { * @description * This method is getter only. * - * Return full url representation with all segments encoded according to rules specified in + * 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 + * // 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 + * @return {string} full URL */ absUrl: locationGetter('$$absUrl'), @@ -13142,18 +13721,18 @@ var locationPrototype = { * @description * This method is getter / setter. * - * Return url (e.g. `/path?a=b#hash`) when called without any parameter. + * 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 + * // 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`) + * @param {string=} url New URL without base prefix (e.g. `/path?a=b#hash`) * @return {string} url */ url: function(url) { @@ -13176,16 +13755,16 @@ var locationPrototype = { * @description * This method is getter only. * - * Return protocol of current url. + * Return protocol of current URL. * * * ```js - * // given url http://example.com/#/some/path?foo=bar&baz=xoxo + * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo * var protocol = $location.protocol(); * // => "http" * ``` * - * @return {string} protocol of current url + * @return {string} protocol of current URL */ protocol: locationGetter('$$protocol'), @@ -13196,24 +13775,24 @@ var locationPrototype = { * @description * This method is getter only. * - * Return host of current url. + * 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 + * // 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 + * // 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. + * @return {string} host of current URL. */ host: locationGetter('$$host'), @@ -13224,11 +13803,11 @@ var locationPrototype = { * @description * This method is getter only. * - * Return port of current url. + * Return port of current URL. * * * ```js - * // given url http://example.com/#/some/path?foo=bar&baz=xoxo + * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo * var port = $location.port(); * // => 80 * ``` @@ -13244,7 +13823,7 @@ var locationPrototype = { * @description * This method is getter / setter. * - * Return path of current url when called without any parameter. + * Return path of current URL when called without any parameter. * * Change path when called with parameter and return `$location`. * @@ -13253,7 +13832,7 @@ var locationPrototype = { * * * ```js - * // given url http://example.com/#/some/path?foo=bar&baz=xoxo + * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo * var path = $location.path(); * // => "/some/path" * ``` @@ -13263,7 +13842,7 @@ var locationPrototype = { */ path: locationGetterSetter('$$path', function(path) { path = path !== null ? path.toString() : ''; - return path.charAt(0) == '/' ? path : '/' + path; + return path.charAt(0) === '/' ? path : '/' + path; }), /** @@ -13273,13 +13852,13 @@ var locationPrototype = { * @description * This method is getter / setter. * - * Return search part (as object) of current url when called without any parameter. + * 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 + * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo * var searchObject = $location.search(); * // => {foo: 'bar', baz: 'xoxo'} * @@ -13295,7 +13874,7 @@ var locationPrototype = { * 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. + * as duplicate search parameters in the URL. * * @param {(string|Number|Array<string>|boolean)=} paramValue If `search` is a string or number, then `paramValue` * will override only a single search property. @@ -13357,7 +13936,7 @@ var locationPrototype = { * * * ```js - * // given url http://example.com/#/some/path?foo=bar&baz=xoxo#hashValue + * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo#hashValue * var hash = $location.hash(); * // => "hashValue" * ``` @@ -13418,6 +13997,7 @@ forEach([LocationHashbangInHtml5Url, LocationHashbangUrl, LocationHtml5Url], fun // but we're changing the $$state reference to $browser.state() during the $digest // so the modification window is narrow. this.$$state = isUndefined(state) ? null : state; + this.$$urlUpdatedByLocation = true; return this; }; @@ -13425,14 +14005,14 @@ forEach([LocationHashbangInHtml5Url, LocationHashbangUrl, LocationHtml5Url], fun function locationGetter(property) { - return function() { + return /** @this */ function() { return this[property]; }; } function locationGetterSetter(property, preprocess) { - return function(value) { + return /** @this */ function(value) { if (isUndefined(value)) { return this[property]; } @@ -13474,11 +14054,13 @@ function locationGetterSetter(property, preprocess) { /** * @ngdoc provider * @name $locationProvider + * @this + * * @description * Use the `$locationProvider` to configure how the application deep linking paths are stored. */ function $LocationProvider() { - var hashPrefix = '', + var hashPrefix = '!', html5Mode = { enabled: false, requireBase: true, @@ -13489,6 +14071,7 @@ function $LocationProvider() { * @ngdoc method * @name $locationProvider#hashPrefix * @description + * The default value for the prefix is `'!'`. * @param {string=} prefix Prefix for hash part (containing path and search) * @returns {*} current value if used as getter or itself (chaining) if used as setter */ @@ -13515,8 +14098,12 @@ function $LocationProvider() { * whether or not a <base> 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}` - (default: `true`) When html5Mode is enabled, - * enables/disables url rewriting for relative links. + * - **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 `<a internal-link>` 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 */ @@ -13534,7 +14121,7 @@ function $LocationProvider() { html5Mode.requireBase = mode.requireBase; } - if (isBoolean(mode.rewriteLinks)) { + if (isBoolean(mode.rewriteLinks) || isString(mode.rewriteLinks)) { html5Mode.rewriteLinks = mode.rewriteLinks; } @@ -13594,7 +14181,7 @@ function $LocationProvider() { if (html5Mode.enabled) { if (!baseHref && html5Mode.requireBase) { throw $locationMinErr('nobase', - "$location in HTML5 mode requires a <base> tag to be present!"); + '$location in HTML5 mode requires a <base> tag to be present!'); } appBase = serverBase(initialUrl) + (baseHref || '/'); LocationMode = $sniffer.history ? LocationHtml5Url : LocationHashbangInHtml5Url; @@ -13631,10 +14218,11 @@ function $LocationProvider() { } $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 (!html5Mode.rewriteLinks || event.ctrlKey || event.metaKey || event.shiftKey || event.which == 2 || event.button == 2) return; + if (!rewriteLinks || event.ctrlKey || event.metaKey || event.shiftKey || event.which === 2 || event.button === 2) return; var elm = jqLite(event.target); @@ -13644,6 +14232,8 @@ function $LocationProvider() { 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 @@ -13665,7 +14255,7 @@ function $LocationProvider() { // getting double entries in the location history. event.preventDefault(); // update location manually - if ($location.absUrl() != $browser.url()) { + 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; @@ -13676,7 +14266,7 @@ function $LocationProvider() { // rewrite hashbang url <> html5 url - if (trimEmptyHash($location.absUrl()) != trimEmptyHash(initialUrl)) { + if (trimEmptyHash($location.absUrl()) !== trimEmptyHash(initialUrl)) { $browser.url($location.absUrl(), true); } @@ -13685,7 +14275,7 @@ function $LocationProvider() { // update $location when $browser url changes $browser.onUrlChange(function(newUrl, newState) { - if (isUndefined(stripBaseUrl(appBaseNoFile, newUrl))) { + if (!startsWith(newUrl, appBaseNoFile)) { // If we are navigating outside of the app then force a reload $window.location.href = newUrl; return; @@ -13720,36 +14310,40 @@ function $LocationProvider() { // 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 || $location.$$urlUpdatedByLocation) { + $location.$$urlUpdatedByLocation = false; - if (initializing || urlOrStateChanged) { - initializing = false; + 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); - $rootScope.$evalAsync(function() { - var newUrl = $location.absUrl(); - var defaultPrevented = $rootScope.$broadcast('$locationChangeStart', newUrl, oldUrl, - $location.$$state, oldState).defaultPrevented; + if (initializing || urlOrStateChanged) { + initializing = false; - // if the location was changed by a `$locationChangeStart` handler then stop - // processing this location change - if ($location.absUrl() !== newUrl) return; + $rootScope.$evalAsync(function() { + var newUrl = $location.absUrl(); + var defaultPrevented = $rootScope.$broadcast('$locationChangeStart', newUrl, oldUrl, + $location.$$state, oldState).defaultPrevented; - if (defaultPrevented) { - $location.$$parse(oldUrl); - $location.$$state = oldState; - } else { - if (urlOrStateChanged) { - setBrowserUrlWithFallback(newUrl, currentReplace, - oldState === $location.$$state ? null : $location.$$state); + // 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); } - afterLocationChange(oldUrl, oldState); - } - }); + }); + } } $location.$$replace = false; @@ -13782,7 +14376,7 @@ function $LocationProvider() { * {@link ng.$logProvider ng.$logProvider#debugEnabled} to change this. * * @example - <example module="logExample"> + <example module="logExample" name="log-service"> <file name="script.js"> angular.module('logExample', []) .controller('LogController', ['$scope', '$log', function($scope, $log) { @@ -13808,6 +14402,8 @@ function $LocationProvider() { /** * @ngdoc provider * @name $logProvider + * @this + * * @description * Use the `$logProvider` to configure how the application logs messages */ @@ -13825,13 +14421,22 @@ function $LogProvider() { this.debugEnabled = function(flag) { if (isDefined(flag)) { debug = flag; - return this; + return this; } else { return debug; } }; this.$get = ['$window', function($window) { + // Support: IE 9-11, Edge 12-14+ + // IE/Edge display errors in such a way that it requires the user to click in 4 places + // to see the stack trace. There is no way to feature-detect it so there's a chance + // of the user agent sniffing to go wrong but since it's only about logging, this shouldn't + // break apps. Other browsers display errors in a sensible way and some of them map stack + // traces along source maps if available so it makes sense to let browsers display it + // as they want. + var formatStackTrace = msie || /\bEdge\//.test($window.navigator && $window.navigator.userAgent); + return { /** * @ngdoc method @@ -13884,12 +14489,12 @@ function $LogProvider() { fn.apply(self, arguments); } }; - }()) + })() }; function formatError(arg) { if (arg instanceof Error) { - if (arg.stack) { + if (arg.stack && formatStackTrace) { arg = (arg.message && arg.stack.indexOf(arg.message) === -1) ? 'Error: ' + arg.message + '\n' + arg.stack : arg.stack; @@ -13909,7 +14514,7 @@ function $LogProvider() { // The reason behind this is that console.log has type "object" in IE8... try { hasApply = !!logFn.apply; - } catch (e) {} + } catch (e) { /* empty */ } if (hasApply) { return function() { @@ -13943,41 +14548,23 @@ function $LogProvider() { var $parseMinErr = minErr('$parse'); +var objectValueOf = {}.constructor.prototype.valueOf; + // Sandboxing Angular Expressions // ------------------------------ -// Angular expressions are generally considered safe because these expressions only have direct -// access to `$scope` and locals. However, one can obtain the ability to execute arbitrary JS code by -// obtaining a reference to native JS functions such as the Function constructor. +// Angular expressions are no longer sandboxed. So it is now even easier to access arbitrary JS code by +// various means such as obtaining a reference to native JS functions like the Function constructor. // // As an example, consider the following Angular expression: // // {}.toString.constructor('alert("evil JS code")') // -// 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. +// It is important to realize that if you create an expression from a string that contains user provided +// content then it is possible that your application contains a security vulnerability to an XSS style attack. // // 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 @@ -13996,64 +14583,10 @@ function getStringValue(name) { 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; -} - -var CALL = Function.prototype.call; -var APPLY = Function.prototype.apply; -var BIND = Function.prototype.bind; - -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 === (0).constructor || obj === (false).constructor || obj === ''.constructor || - obj === {}.constructor || obj === [].constructor || obj === Function.constructor) { - throw $parseMinErr('isecaf', - 'Assigning to a constructor 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", "'":"'", '"':'"'}; +var ESCAPE = {'n':'\n', 'f':'\f', 'r':'\r', 't':'\t', 'v':'\v', '\'':'\'', '"':'"'}; ///////////////////////////////////////// @@ -14062,7 +14595,7 @@ var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"' /** * @constructor */ -var Lexer = function(options) { +var Lexer = function Lexer(options) { this.options = options; }; @@ -14076,7 +14609,7 @@ Lexer.prototype = { while (this.index < this.text.length) { var ch = this.text.charAt(this.index); - if (ch === '"' || ch === "'") { + if (ch === '"' || ch === '\'') { this.readString(ch); } else if (this.isNumber(ch) || ch === '.' && this.isNumber(this.peek())) { this.readNumber(); @@ -14115,7 +14648,7 @@ Lexer.prototype = { }, isNumber: function(ch) { - return ('0' <= ch && ch <= '9') && typeof ch === "string"; + return ('0' <= ch && ch <= '9') && typeof ch === 'string'; }, isWhitespace: function(ch) { @@ -14148,9 +14681,8 @@ Lexer.prototype = { codePointAt: function(ch) { if (ch.length === 1) return ch.charCodeAt(0); - /*jshint bitwise: false*/ + // eslint-disable-next-line no-bitwise return (ch.charCodeAt(0) << 10) + ch.charCodeAt(1) - 0x35FDC00; - /*jshint bitwise: true*/ }, peekMultichar: function() { @@ -14185,19 +14717,19 @@ Lexer.prototype = { var start = this.index; while (this.index < this.text.length) { var ch = lowercase(this.text.charAt(this.index)); - if (ch == '.' || this.isNumber(ch)) { + if (ch === '.' || this.isNumber(ch)) { number += ch; } else { var peekCh = this.peek(); - if (ch == 'e' && this.isExpOperator(peekCh)) { + if (ch === 'e' && this.isExpOperator(peekCh)) { number += ch; } else if (this.isExpOperator(ch) && peekCh && this.isNumber(peekCh) && - number.charAt(number.length - 1) == 'e') { + number.charAt(number.length - 1) === 'e') { number += ch; } else if (this.isExpOperator(ch) && (!peekCh || !this.isNumber(peekCh)) && - number.charAt(number.length - 1) == 'e') { + number.charAt(number.length - 1) === 'e') { this.throwError('Invalid exponent'); } else { break; @@ -14272,7 +14804,7 @@ Lexer.prototype = { } }; -var AST = function(lexer, options) { +var AST = function AST(lexer, options) { this.lexer = lexer; this.options = options; }; @@ -14328,8 +14860,7 @@ AST.prototype = { filterChain: function() { var left = this.expression(); - var token; - while ((token = this.expect('|'))) { + while (this.expect('|')) { left = this.filter(left); } return left; @@ -14342,6 +14873,10 @@ AST.prototype = { 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; @@ -14541,7 +15076,7 @@ AST.prototype = { this.consume(':'); property.value = this.expression(); } else { - this.throwError("invalid key", this.peek()); + this.throwError('invalid key', this.peek()); } properties.push(property); } while (this.expect(',')); @@ -14625,6 +15160,7 @@ function isStateless($filter, filterName) { function findConstantAndWatchExpressions(ast, $filter) { var allConstants; var argsToWatch; + var isStatelessFilter; switch (ast.type) { case AST.Program: allConstants = true; @@ -14675,7 +15211,8 @@ function findConstantAndWatchExpressions(ast, $filter) { ast.toWatch = [ast]; break; case AST.CallExpression: - allConstants = ast.filter ? isStateless($filter, ast.callee.name) : false; + isStatelessFilter = ast.filter ? isStateless($filter, ast.callee.name) : false; + allConstants = isStatelessFilter; argsToWatch = []; forEach(ast.arguments, function(expr) { findConstantAndWatchExpressions(expr, $filter); @@ -14685,7 +15222,7 @@ function findConstantAndWatchExpressions(ast, $filter) { } }); ast.constant = allConstants; - ast.toWatch = ast.filter && isStateless($filter, ast.callee.name) ? argsToWatch : [ast]; + ast.toWatch = isStatelessFilter ? argsToWatch : [ast]; break; case AST.AssignmentExpression: findConstantAndWatchExpressions(ast.left, $filter); @@ -14715,6 +15252,13 @@ function findConstantAndWatchExpressions(ast, $filter) { if (!property.value.constant) { argsToWatch.push.apply(argsToWatch, property.value.toWatch); } + if (property.computed) { + findConstantAndWatchExpressions(property.key, $filter); + if (!property.key.constant) { + argsToWatch.push.apply(argsToWatch, property.key.toWatch); + } + } + }); ast.constant = allConstants; ast.toWatch = argsToWatch; @@ -14731,7 +15275,7 @@ function findConstantAndWatchExpressions(ast, $filter) { } function getInputs(body) { - if (body.length != 1) return; + if (body.length !== 1) return; var lastExpression = body[0].expression; var candidate = lastExpression.toWatch; if (candidate.length !== 1) return candidate; @@ -14760,19 +15304,16 @@ function isConstant(ast) { return ast.constant; } -function ASTCompiler(astBuilder, $filter) { - this.astBuilder = astBuilder; +function ASTCompiler($filter) { this.$filter = $filter; } ASTCompiler.prototype = { - compile: function(expression, expensiveChecks) { + compile: function(ast) { 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: [] @@ -14813,30 +15354,17 @@ ASTCompiler.prototype = { this.watchFns() + 'return fn;'; - /* jshint -W054 */ + // 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); - /* jshint +W054 */ + plusFn); this.state = this.stage = undefined; - fn.literal = isLiteral(ast); - fn.constant = isConstant(ast); return fn; }, @@ -14907,7 +15435,7 @@ ASTCompiler.prototype = { case AST.Literal: expression = this.escape(ast.value); this.assign(intoId, expression); - recursionFn(expression); + recursionFn(intoId || expression); break; case AST.UnaryExpression: this.recurse(ast.argument, undefined, undefined, function(expr) { right = expr; }); @@ -14947,22 +15475,18 @@ ASTCompiler.prototype = { 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.isNull(self.nonComputedMember('s', ast.name)), self.lazyAssign(self.nonComputedMember('s', ast.name), '{}')); } self.assign(intoId, self.nonComputedMember('s', ast.name)); }); }, intoId && self.lazyAssign(intoId, self.nonComputedMember('l', ast.name)) ); - if (self.state.expensiveChecks || isPossiblyDangerousMemberName(ast.name)) { - self.addEnsureSafeObject(intoId); - } recursionFn(intoId); break; case AST.MemberExpression: @@ -14970,32 +15494,24 @@ ASTCompiler.prototype = { 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)); + expression = 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), '{}')); + self.if_(self.isNull(self.nonComputedMember(left, ast.property.name)), self.lazyAssign(self.nonComputedMember(left, ast.property.name), '{}')); } expression = self.nonComputedMember(left, ast.property.name); - if (self.state.expensiveChecks || isPossiblyDangerousMemberName(ast.property.name)) { - expression = self.ensureSafeObject(expression); - } self.assign(intoId, expression); if (nameId) { nameId.computed = false; @@ -15027,21 +15543,16 @@ ASTCompiler.prototype = { 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)); + self.recurse(expr, ast.constant ? undefined : self.nextId(), undefined, function(argument) { + args.push(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'); @@ -15053,14 +15564,9 @@ ASTCompiler.prototype = { case AST.AssignmentExpression: right = this.nextId(); left = {}; - if (!isAssignable(ast.left)) { - throw $parseMinErr('lval', 'Trying to assign a value to a non l-value'); - } 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); @@ -15070,13 +15576,13 @@ ASTCompiler.prototype = { case AST.ArrayExpression: args = []; forEach(ast.elements, function(expr) { - self.recurse(expr, self.nextId(), undefined, function(argument) { + self.recurse(expr, ast.constant ? undefined : self.nextId(), undefined, function(argument) { args.push(argument); }); }); expression = '[' + args.join(',') + ']'; this.assign(intoId, expression); - recursionFn(expression); + recursionFn(intoId || expression); break; case AST.ObjectExpression: args = []; @@ -15118,15 +15624,15 @@ ASTCompiler.prototype = { break; case AST.ThisExpression: this.assign(intoId, 's'); - recursionFn('s'); + recursionFn(intoId || 's'); break; case AST.LocalsExpression: this.assign(intoId, 'l'); - recursionFn('l'); + recursionFn(intoId || 'l'); break; case AST.NGValueParameter: this.assign(intoId, 'v'); - recursionFn('v'); + recursionFn(intoId || 'v'); break; } }, @@ -15185,12 +15691,16 @@ ASTCompiler.prototype = { return '!(' + expression + ')'; }, + isNull: function(expression) { + return expression + '==null'; + }, + notNull: function(expression) { return expression + '!=null'; }, nonComputedMember: function(left, right) { - var SAFE_IDENTIFIER = /[$_a-zA-Z][$_a-zA-Z0-9]*/; + var 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; @@ -15208,42 +15718,10 @@ ASTCompiler.prototype = { 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() { @@ -15265,7 +15743,7 @@ ASTCompiler.prototype = { }, escape: function(value) { - if (isString(value)) return "'" + value.replace(this.stringEscapeRegex, this.stringEscapeFn) + "'"; + 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'; @@ -15289,17 +15767,13 @@ ASTCompiler.prototype = { }; -function ASTInterpreter(astBuilder, $filter) { - this.astBuilder = astBuilder; +function ASTInterpreter($filter) { this.$filter = $filter; } ASTInterpreter.prototype = { - compile: function(expression, expensiveChecks) { + compile: function(ast) { var self = this; - var ast = this.astBuilder.ast(expression); - this.expression = expression; - this.expensiveChecks = expensiveChecks; findConstantAndWatchExpressions(ast, self.$filter); var assignable; var assign; @@ -15338,13 +15812,11 @@ ASTInterpreter.prototype = { 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, expression; + var left, right, self = this, args; if (ast.input) { return this.inputs(ast.input, ast.watchId); } @@ -15370,20 +15842,16 @@ ASTInterpreter.prototype = { context ); case AST.Identifier: - ensureSafeMemberName(ast.name, self.expression); - return self.identifier(ast.name, - self.expensiveChecks || isPossiblyDangerousMemberName(ast.name), - context, create, self.expression); + return self.identifier(ast.name, context, create); 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); + this.computedMember(left, right, context, create) : + this.nonComputedMember(left, right, context, create); case AST.CallExpression: args = []; forEach(ast.arguments, function(expr) { @@ -15404,13 +15872,11 @@ ASTInterpreter.prototype = { 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)); + values.push(args[i](scope, locals, assign, inputs)); } - value = ensureSafeObject(rhs.value.apply(rhs.context, values), self.expression); + value = rhs.value.apply(rhs.context, values); } return context ? {value: value} : value; }; @@ -15420,8 +15886,6 @@ ASTInterpreter.prototype = { 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; }; @@ -15497,7 +15961,7 @@ ASTInterpreter.prototype = { if (isDefined(arg)) { arg = -arg; } else { - arg = 0; + arg = -0; } return context ? {value: arg} : arg; }; @@ -15556,12 +16020,14 @@ ASTInterpreter.prototype = { }, '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; }; @@ -15611,16 +16077,13 @@ ASTInterpreter.prototype = { value: function(value, context) { return function() { return context ? {context: undefined, name: undefined, value: value} : value; }; }, - identifier: function(name, expensiveChecks, context, create, expression) { + identifier: function(name, context, create) { return function(scope, locals, assign, inputs) { var base = locals && (name in locals) ? locals : scope; - if (create && create !== 1 && base && !(base[name])) { + if (create && create !== 1 && base && base[name] == null) { base[name] = {}; } var value = base ? base[name] : undefined; - if (expensiveChecks) { - ensureSafeObject(value, expression); - } if (context) { return {context: base, name: name, value: value}; } else { @@ -15628,7 +16091,7 @@ ASTInterpreter.prototype = { } }; }, - computedMember: function(left, right, context, create, expression) { + computedMember: function(left, right, context, create) { return function(scope, locals, assign, inputs) { var lhs = left(scope, locals, assign, inputs); var rhs; @@ -15636,15 +16099,12 @@ ASTInterpreter.prototype = { 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}; @@ -15653,19 +16113,15 @@ ASTInterpreter.prototype = { } }; }, - nonComputedMember: function(left, right, expensiveChecks, context, create, expression) { + nonComputedMember: function(left, right, context, create) { return function(scope, locals, assign, inputs) { var lhs = left(scope, locals, assign, inputs); if (create && create !== 1) { - ensureSafeAssignContext(lhs); - if (lhs && !(lhs[right])) { + if (lhs && lhs[right] == null) { 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 { @@ -15684,29 +16140,24 @@ ASTInterpreter.prototype = { /** * @constructor */ -var Parser = function(lexer, $filter, options) { - this.lexer = lexer; - this.$filter = $filter; - this.options = options; +function Parser(lexer, $filter, options) { this.ast = new AST(lexer, options); - this.astCompiler = options.csp ? new ASTInterpreter(this.ast, $filter) : - new ASTCompiler(this.ast, $filter); -}; + this.astCompiler = options.csp ? new ASTInterpreter($filter) : + new ASTCompiler($filter); +} Parser.prototype = { constructor: Parser, parse: function(text) { - return this.astCompiler.compile(text, this.options.expensiveChecks); + var ast = this.ast.ast(text); + var fn = this.astCompiler.compile(ast); + fn.literal = isLiteral(ast); + fn.constant = isConstant(ast); + return fn; } }; -function isPossiblyDangerousMemberName(name) { - return name == 'constructor'; -} - -var objectValueOf = Object.prototype.valueOf; - function getValueOf(value) { return isFunction(value.valueOf) ? value.valueOf() : objectValueOf.call(value); } @@ -15757,14 +16208,14 @@ function getValueOf(value) { /** * @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 cache = createMap(); var literals = { 'true': true, 'false': false, @@ -15791,6 +16242,7 @@ function $ParseProvider() { /** * @ngdoc method * @name $parseProvider#setIdentifierFns + * * @description * * Allows defining the set of characters that are allowed in Angular expressions. The function @@ -15803,7 +16255,7 @@ function $ParseProvider() { * 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 extensivelly, keep the implementation of these functions fast, + * 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 @@ -15821,37 +16273,20 @@ function $ParseProvider() { 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) { + function $parse(exp, interceptorFn) { 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) { @@ -15859,21 +16294,17 @@ function $ParseProvider() { oneTime = true; exp = exp.substring(2); } - var parseOptions = expensiveChecks ? $parseOptionsExpensive : $parseOptions; - var lexer = new Lexer(parseOptions); - var parser = new Parser(lexer, $filter, 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; + parsedExpression.oneTime = true; + parsedExpression.$$watchDelegate = oneTimeWatchDelegate; } else if (parsedExpression.inputs) { parsedExpression.$$watchDelegate = inputsWatchDelegate; } - if (expensiveChecks) { - parsedExpression = expensiveChecksInterceptor(parsedExpression); - } cache[cacheKey] = parsedExpression; } return addInterceptor(parsedExpression, interceptorFn); @@ -15886,31 +16317,7 @@ function $ParseProvider() { } } - 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) { + function expressionInputDirtyCheck(newValue, oldValueOfValue, compareObjectIdentity) { if (newValue == null || oldValueOfValue == null) { // null/undefined return newValue === oldValueOfValue; @@ -15923,7 +16330,7 @@ function $ParseProvider() { // be cheaply dirty-checked newValue = getValueOf(newValue); - if (typeof newValue === 'object') { + if (typeof newValue === 'object' && !compareObjectIdentity) { // objects/arrays are not supported - deep-watching them would be too expensive return false; } @@ -15932,6 +16339,7 @@ function $ParseProvider() { } //Primitive or NaN + // eslint-disable-next-line no-self-compare return newValue === oldValueOfValue || (newValue !== newValue && oldValueOfValue !== oldValueOfValue); } @@ -15944,7 +16352,7 @@ function $ParseProvider() { inputExpressions = inputExpressions[0]; return scope.$watch(function expressionInputWatch(scope) { var newInputValue = inputExpressions(scope); - if (!expressionInputDirtyCheck(newInputValue, oldInputValueOf)) { + if (!expressionInputDirtyCheck(newInputValue, oldInputValueOf, parsedExpression.literal)) { lastResult = parsedExpression(scope, undefined, undefined, [newInputValue]); oldInputValueOf = newInputValue && getValueOf(newInputValue); } @@ -15964,7 +16372,7 @@ function $ParseProvider() { for (var i = 0, ii = inputExpressions.length; i < ii; i++) { var newInputValue = inputExpressions[i](scope); - if (changed || (changed = !expressionInputDirtyCheck(newInputValue, oldInputValueOfValues[i]))) { + if (changed || (changed = !expressionInputDirtyCheck(newInputValue, oldInputValueOfValues[i], parsedExpression.literal))) { oldInputValues[i] = newInputValue; oldInputValueOfValues[i] = newInputValue && getValueOf(newInputValue); } @@ -15978,56 +16386,48 @@ function $ParseProvider() { }, listener, objectEquality, prettyPrintExpression); } - function oneTimeWatchDelegate(scope, listener, objectEquality, parsedExpression) { + function oneTimeWatchDelegate(scope, listener, objectEquality, parsedExpression, prettyPrintExpression) { + var isDone = parsedExpression.literal ? isAllDefined : isDefined; var unwatch, lastValue; - return unwatch = scope.$watch(function oneTimeWatch(scope) { + if (parsedExpression.inputs) { + unwatch = inputsWatchDelegate(scope, oneTimeListener, objectEquality, parsedExpression, prettyPrintExpression); + } else { + unwatch = scope.$watch(oneTimeWatch, oneTimeListener, objectEquality); + } + return unwatch; + + function oneTimeWatch(scope) { return parsedExpression(scope); - }, function oneTimeListener(value, old, scope) { + } + function oneTimeListener(value, old, scope) { lastValue = value; if (isFunction(listener)) { - listener.apply(this, arguments); + listener(value, old, scope); } - if (isDefined(value)) { + if (isDone(value)) { scope.$$postDigest(function() { - if (isDefined(lastValue)) { + if (isDone(lastValue)) { unwatch(); } }); } - }, objectEquality); + } } - function oneTimeLiteralWatchDelegate(scope, listener, objectEquality, parsedExpression) { - var unwatch, lastValue; - return unwatch = scope.$watch(function oneTimeWatch(scope) { - return parsedExpression(scope); - }, 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); - - function isAllDefined(value) { - var allDefined = true; - forEach(value, function(val) { - if (!isDefined(val)) allDefined = false; - }); - return allDefined; - } + 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; - return unwatch = scope.$watch(function constantWatch(scope) { + var unwatch = scope.$watch(function constantWatch(scope) { unwatch(); return parsedExpression(scope); }, listener, objectEquality); + return unwatch; } function addInterceptor(parsedExpression, interceptorFn) { @@ -16035,30 +16435,36 @@ function $ParseProvider() { var watchDelegate = parsedExpression.$$watchDelegate; var useInputs = false; - var regularWatch = - watchDelegate !== oneTimeLiteralWatchDelegate && - watchDelegate !== oneTimeWatchDelegate; + var isDone = parsedExpression.literal ? isAllDefined : isDefined; - var fn = regularWatch ? function regularInterceptedExpression(scope, locals, assign, inputs) { + 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); + } + + function oneTimeInterceptedExpression(scope, locals, assign, inputs) { + var value = useInputs && inputs ? inputs[0] : 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; - }; + return isDone(value) ? result : value; + } - // Propagate $$watchDelegates other then inputsWatchDelegate - if (parsedExpression.$$watchDelegate && - parsedExpression.$$watchDelegate !== inputsWatchDelegate) { - fn.$$watchDelegate = parsedExpression.$$watchDelegate; + var fn = parsedExpression.oneTime ? oneTimeInterceptedExpression : regularInterceptedExpression; + + // Propogate the literal/oneTime attributes + fn.literal = parsedExpression.literal; + fn.oneTime = parsedExpression.oneTime; + + // Propagate or create inputs / $$watchDelegates + useInputs = !parsedExpression.inputs; + if (watchDelegate && watchDelegate !== inputsWatchDelegate) { + fn.$$watchDelegate = watchDelegate; + fn.inputs = parsedExpression.inputs; } 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]; } @@ -16076,8 +16482,8 @@ function $ParseProvider() { * 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). + * This is a [Promises/A+](https://promisesaplus.com/)-compliant implementation of promises/deferred + * objects inspired by [Kris Kowal's Q](https://github.com/kriskowal/q). * * $q can be used in two fashions --- one which is more similar to Kris Kowal's Q or jQuery's Deferred * implementations, and the other which resembles ES6 (ES2015) promises to some degree. @@ -16284,21 +16690,61 @@ function $ParseProvider() { * * @returns {Promise} The newly created promise. */ +/** + * @ngdoc provider + * @name $qProvider + * @this + * + * @description + */ function $QProvider() { - + var errorOnUnhandledRejections = true; this.$get = ['$rootScope', '$exceptionHandler', function($rootScope, $exceptionHandler) { return qFactory(function(callback) { $rootScope.$evalAsync(callback); - }, $exceptionHandler); + }, $exceptionHandler, errorOnUnhandledRejections); }]; + + /** + * @ngdoc method + * @name $qProvider#errorOnUnhandledRejections + * @kind function + * + * @description + * Retrieves or overrides whether to generate an error when a rejected promise is not handled. + * This feature is enabled by default. + * + * @param {boolean=} value Whether to generate an error when a rejected promise is not handled. + * @returns {boolean|ng.$qProvider} Current value when called without a new value or self for + * chaining otherwise. + */ + this.errorOnUnhandledRejections = function(value) { + if (isDefined(value)) { + errorOnUnhandledRejections = value; + return this; + } else { + return errorOnUnhandledRejections; + } + }; } +/** @this */ function $$QProvider() { + var errorOnUnhandledRejections = true; this.$get = ['$browser', '$exceptionHandler', function($browser, $exceptionHandler) { return qFactory(function(callback) { $browser.defer(callback); - }, $exceptionHandler); + }, $exceptionHandler, errorOnUnhandledRejections); }]; + + this.errorOnUnhandledRejections = function(value) { + if (isDefined(value)) { + errorOnUnhandledRejections = value; + return this; + } else { + return errorOnUnhandledRejections; + } + }; } /** @@ -16307,10 +16753,14 @@ function $$QProvider() { * @param {function(function)} nextTick Function for executing functions in the next turn. * @param {function(...*)} exceptionHandler Function into which unexpected exceptions are passed for * debugging purposes. + @ param {=boolean} errorOnUnhandledRejections Whether an error should be generated on unhandled + * promises rejections. * @returns {object} Promise manager. */ -function qFactory(nextTick, exceptionHandler) { +function qFactory(nextTick, exceptionHandler, errorOnUnhandledRejections) { var $qMinErr = minErr('$q', TypeError); + var queueSize = 0; + var checkQueue = []; /** * @ngdoc method @@ -16322,14 +16772,18 @@ function qFactory(nextTick, exceptionHandler) { * * @returns {Deferred} Returns a new instance of deferred. */ - var defer = function() { - 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 defer() { + return new Deferred(); + } + + function Deferred() { + var promise = this.promise = new Promise(); + //Non prototype methods necessary to support unbound execution :/ + this.resolve = function(val) { resolvePromise(promise, val); }; + this.reject = function(reason) { rejectPromise(promise, reason); }; + this.notify = function(progress) { notifyPromise(promise, progress); }; + } + function Promise() { this.$$state = { status: 0 }; @@ -16340,144 +16794,162 @@ function qFactory(nextTick, exceptionHandler) { if (isUndefined(onFulfilled) && isUndefined(onRejected) && isUndefined(progressBack)) { return this; } - var result = new Deferred(); + var result = new Promise(); 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; + return result; }, - "catch": function(callback) { + 'catch': function(callback) { return this.then(null, callback); }, - "finally": function(callback, progressBack) { + 'finally': function(callback, progressBack) { return this.then(function(value) { - return handleCallback(value, true, callback); + return handleCallback(value, resolve, callback); }, function(error) { - return handleCallback(error, false, callback); + 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; + var fn, promise, 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); + try { + for (var i = 0, ii = pending.length; i < ii; ++i) { + state.pur = true; + promise = pending[i][0]; + fn = pending[i][state.status]; + try { + if (isFunction(fn)) { + resolvePromise(promise, fn(state.value)); + } else if (state.status === 1) { + resolvePromise(promise, state.value); + } else { + rejectPromise(promise, state.value); + } + } catch (e) { + rejectPromise(promise, e); + } + } + } finally { + --queueSize; + if (errorOnUnhandledRejections && queueSize === 0) { + nextTick(processChecks); + } + } + } + + function processChecks() { + // eslint-disable-next-line no-unmodified-loop-condition + while (!queueSize && checkQueue.length) { + var toCheck = checkQueue.shift(); + if (!toCheck.pur) { + toCheck.pur = true; + var errorMessage = 'Possibly unhandled rejection: ' + toDebugString(toCheck.value); + if (toCheck.value instanceof Error) { + exceptionHandler(toCheck.value, errorMessage); } else { - deferred.reject(state.value); + exceptionHandler(errorMessage); } - } catch (e) { - deferred.reject(e); - exceptionHandler(e); } } } function scheduleProcessQueue(state) { + if (errorOnUnhandledRejections && !state.pending && state.status === 2 && !state.pur) { + if (queueSize === 0 && checkQueue.length === 0) { + nextTick(processChecks); + } + checkQueue.push(state); + } if (state.processScheduled || !state.pending) return; state.processScheduled = true; + ++queueSize; nextTick(function() { processQueue(state); }); } - function Deferred() { - this.promise = new Promise(); + function resolvePromise(promise, val) { + if (promise.$$state.status) return; + if (val === promise) { + $$reject(promise, $qMinErr( + 'qcycle', + 'Expected promise to be resolved with value other than itself \'{0}\'', + val)); + } else { + $$resolve(promise, val); + } + } - 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)); + function $$resolve(promise, val) { + var then; + var done = false; + try { + if (isObject(val) || isFunction(val)) then = val.then; + if (isFunction(then)) { + promise.$$state.status = -1; + then.call(val, doResolve, doReject, doNotify); } else { - 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); + promise.$$state.value = val; + promise.$$state.status = 1; + scheduleProcessQueue(promise.$$state); } + } catch (e) { + doReject(e); + } - function resolvePromise(val) { - if (done) return; - done = true; - that.$$resolve(val); - } - function rejectPromise(val) { - if (done) return; - done = true; - that.$$reject(val); - } - }, + function doResolve(val) { + if (done) return; + done = true; + $$resolve(promise, val); + } + function doReject(val) { + if (done) return; + done = true; + $$reject(promise, val); + } + function doNotify(progress) { + notifyPromise(promise, progress); + } + } - reject: function(reason) { - if (this.promise.$$state.status) return; - this.$$reject(reason); - }, + function rejectPromise(promise, reason) { + if (promise.$$state.status) return; + $$reject(promise, reason); + } - $$reject: function(reason) { - this.promise.$$state.value = reason; - this.promise.$$state.status = 2; - scheduleProcessQueue(this.promise.$$state); - }, + function $$reject(promise, reason) { + promise.$$state.value = reason; + promise.$$state.status = 2; + scheduleProcessQueue(promise.$$state); + } - notify: function(progress) { - var callbacks = this.promise.$$state.pending; + function notifyPromise(promise, progress) { + var callbacks = 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); - } + if ((promise.$$state.status <= 0) && callbacks && callbacks.length) { + nextTick(function() { + var callback, result; + for (var i = 0, ii = callbacks.length; i < ii; i++) { + result = callbacks[i][0]; + callback = callbacks[i][3]; + try { + notifyPromise(result, isFunction(callback) ? callback(progress) : progress); + } catch (e) { + exceptionHandler(e); } - }); - } + } + }); } - }); + } /** * @ngdoc method @@ -16515,39 +16987,27 @@ function qFactory(nextTick, exceptionHandler) { * @param {*} reason Constant, message, exception or an object representing the rejection reason. * @returns {Promise} Returns a promise that was already resolved as rejected with the `reason`. */ - var reject = function(reason) { - var result = new Deferred(); - result.reject(reason); - return result.promise; - }; - - var makePromise = function makePromise(value, resolved) { - var result = new Deferred(); - if (resolved) { - result.resolve(value); - } else { - result.reject(value); - } - return result.promise; - }; + function reject(reason) { + var result = new Promise(); + rejectPromise(result, reason); + return result; + } - var handleCallback = function handleCallback(value, isResolved, callback) { + function handleCallback(value, resolver, callback) { var callbackOutput = null; try { if (isFunction(callback)) callbackOutput = callback(); } catch (e) { - return makePromise(e, false); + return reject(e); } if (isPromiseLike(callbackOutput)) { return callbackOutput.then(function() { - return makePromise(value, isResolved); - }, function(error) { - return makePromise(error, false); - }); + return resolver(value); + }, reject); } else { - return makePromise(value, isResolved); + return resolver(value); } - }; + } /** * @ngdoc method @@ -16567,11 +17027,11 @@ function qFactory(nextTick, exceptionHandler) { */ - var when = function(value, callback, errback, progressBack) { - var result = new Deferred(); - result.resolve(value); - return result.promise.then(callback, errback, progressBack); - }; + function when(value, callback, errback, progressBack) { + var result = new Promise(); + resolvePromise(result, value); + return result.then(callback, errback, progressBack); + } /** * @ngdoc method @@ -16606,27 +17066,25 @@ function qFactory(nextTick, exceptionHandler) { */ function all(promises) { - var deferred = new Deferred(), + var result = new Promise(), counter = 0, results = isArray(promises) ? [] : {}; forEach(promises, function(promise, key) { counter++; when(promise).then(function(value) { - if (results.hasOwnProperty(key)) return; results[key] = value; - if (!(--counter)) deferred.resolve(results); + if (!(--counter)) resolvePromise(result, results); }, function(reason) { - if (results.hasOwnProperty(key)) return; - deferred.reject(reason); + rejectPromise(result, reason); }); }); if (counter === 0) { - deferred.resolve(results); + resolvePromise(result, results); } - return deferred.promise; + return result; } /** @@ -16653,25 +17111,25 @@ function qFactory(nextTick, exceptionHandler) { return deferred.promise; } - var $Q = function Q(resolver) { + function $Q(resolver) { if (!isFunction(resolver)) { - throw $qMinErr('norslvr', "Expected resolverFn, got '{0}'", resolver); + throw $qMinErr('norslvr', 'Expected resolverFn, got \'{0}\'', resolver); } - var deferred = new Deferred(); + var promise = new Promise(); function resolveFn(value) { - deferred.resolve(value); + resolvePromise(promise, value); } function rejectFn(reason) { - deferred.reject(reason); + rejectPromise(promise, reason); } resolver(resolveFn, rejectFn); - return deferred.promise; - }; + return promise; + } // Let's make the instanceof operator work for promises, so that // `new $q(fn) instanceof $q` would evaluate to true. @@ -16687,6 +17145,7 @@ function qFactory(nextTick, exceptionHandler) { return $Q; } +/** @this */ function $$RAFProvider() { //rAF this.$get = ['$window', '$timeout', function($window, $timeout) { var requestAnimationFrame = $window.requestAnimationFrame || @@ -16776,6 +17235,8 @@ function $$RAFProvider() { //rAF /** * @ngdoc service * @name $rootScope + * @this + * * @description * * Every application has a single root {@link ng.$rootScope.Scope scope}. @@ -16820,14 +17281,19 @@ function $RootScopeProvider() { function cleanUpScope($scope) { + // Support: IE 9 only if (msie === 9) { // There is a memory leak in IE9 if all child scopes are not disconnected // completely when a scope is destroyed. So this code will recurse up through // all this scopes children // // See issue https://github.com/angular/angular.js/issues/10706 - $scope.$$childHead && cleanUpScope($scope.$$childHead); - $scope.$$nextSibling && cleanUpScope($scope.$$nextSibling); + if ($scope.$$childHead) { + cleanUpScope($scope.$$childHead); + } + if ($scope.$$nextSibling) { + cleanUpScope($scope.$$nextSibling); + } } // The code below works around IE9 and V8's memory leaks @@ -16979,7 +17445,7 @@ function $RootScopeProvider() { // 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); + if (isolate || parent !== this) child.$on('$destroy', destroyChildScope); return child; }, @@ -16996,7 +17462,7 @@ function $RootScopeProvider() { * $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). + * [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, @@ -17007,6 +17473,8 @@ function $RootScopeProvider() { * according to the {@link angular.equals} function. To save the value of the object for * later comparison, the {@link angular.copy} function is used. This therefore means that * watching complex objects will have adverse memory and performance implications. + * - This should not be used to watch for changes in objects that are + * or contain [File](https://developer.mozilla.org/docs/Web/API/File) objects due to limitations with {@link angular.copy `angular.copy`}. * - The watch `listener` may change the model, which may trigger other `listener`s to fire. * This is achieved by rerunning the watchers until no changes are detected. The rerun * iteration limit is 10 to prevent an infinite loop deadlock. @@ -17124,15 +17592,21 @@ function $RootScopeProvider() { 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() { - if (arrayRemove(array, watcher) >= 0) { + var index = arrayRemove(array, watcher); + if (index >= 0) { incrementWatchersCount(scope, -1); + if (index < array.$$digestWatchIndex) { + array.$$digestWatchIndex--; + } } lastDirtyWatch = null; }; @@ -17147,8 +17621,8 @@ function $RootScopeProvider() { * 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 standard $watch operation and are examined on every - * call to $digest() to see if any items changes. + * - 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.<string|Function(scope)>} watchExpressions Array of expressions that will be individually @@ -17329,6 +17803,7 @@ function $RootScopeProvider() { oldItem = oldValue[i]; newItem = newValue[i]; + // eslint-disable-next-line no-self-compare bothNaN = (oldItem !== oldItem) && (newItem !== newItem); if (!bothNaN && (oldItem !== newItem)) { changeDetected++; @@ -17351,6 +17826,7 @@ function $RootScopeProvider() { oldItem = oldValue[key]; if (key in oldValue) { + // eslint-disable-next-line no-self-compare bothNaN = (oldItem !== oldItem) && (newItem !== newItem); if (!bothNaN && (oldItem !== newItem)) { changeDetected++; @@ -17463,7 +17939,6 @@ function $RootScopeProvider() { $digest: function() { var watch, value, last, fn, get, watchers, - length, dirty, ttl = TTL, next, current, target = this, watchLog = [], @@ -17487,12 +17962,13 @@ function $RootScopeProvider() { 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 + // be reentered recursively. Calling $digest from a function passed to $evalAsync would // lead to a '$digest already in progress' error. for (var asyncQueuePosition = 0; asyncQueuePosition < asyncQueue.length; asyncQueuePosition++) { try { asyncTask = asyncQueue[asyncQueuePosition]; - asyncTask.scope.$eval(asyncTask.expression, asyncTask.locals); + fn = asyncTask.fn; + fn(asyncTask.scope, asyncTask.locals); } catch (e) { $exceptionHandler(e); } @@ -17504,10 +17980,10 @@ function $RootScopeProvider() { do { // "traverse the scopes" loop if ((watchers = current.$$watchers)) { // process our watches - length = watchers.length; - while (length--) { + watchers.$$digestWatchIndex = watchers.length; + while (watchers.$$digestWatchIndex--) { try { - watch = watchers[length]; + 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) { @@ -17515,8 +17991,7 @@ function $RootScopeProvider() { if ((value = get(current)) !== (last = watch.last) && !(watch.eq ? equals(value, last) - : (typeof value === 'number' && typeof last === 'number' - && isNaN(value) && isNaN(last)))) { + : (isNumberNaN(value) && isNumberNaN(last)))) { dirty = true; lastDirtyWatch = watch; watch.last = watch.eq ? copy(value, null) : value; @@ -17578,6 +18053,10 @@ function $RootScopeProvider() { } } postDigestQueue.length = postDigestQueuePosition = 0; + + // Check for changes to browser url that happened during the $digest + // (for which no event is fired; e.g. via `history.pushState()`) + $browser.$$checkUrlChange(); }, @@ -17635,8 +18114,8 @@ function $RootScopeProvider() { // 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 (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; @@ -17723,7 +18202,7 @@ function $RootScopeProvider() { }); } - asyncQueue.push({scope: this, expression: $parse(expr), locals: locals}); + asyncQueue.push({scope: this, fn: $parse(expr), locals: locals}); }, $$postDigest: function(fn) { @@ -17790,6 +18269,7 @@ function $RootScopeProvider() { $rootScope.$digest(); } catch (e) { $exceptionHandler(e); + // eslint-disable-next-line no-unsafe-finally throw e; } } @@ -17814,7 +18294,9 @@ function $RootScopeProvider() { */ $applyAsync: function(expr) { var scope = this; - expr && applyAsyncQueue.push($applyAsyncExpression); + if (expr) { + applyAsyncQueue.push($applyAsyncExpression); + } expr = $parse(expr); scheduleApplyAsync(); @@ -18108,6 +18590,7 @@ function $RootScopeProvider() { // the implementation is in angular.bootstrap /** + * @this * @description * Private service to sanitize uris for links and images. Used by $compile and $sanitize. */ @@ -18188,20 +18671,38 @@ function $$SanitizeUriProvider() { * Or gives undesired access to variables likes document or window? * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ +/* exported $SceProvider, $SceDelegateProvider */ + var $sceMinErr = minErr('$sce'); var SCE_CONTEXTS = { + // HTML is used when there's HTML rendered (e.g. ng-bind-html, iframe srcdoc binding). HTML: 'html', + + // Style statements or stylesheets. Currently unused in AngularJS. CSS: 'css', + + // An URL used in a context where it does not refer to a resource that loads code. Currently + // unused in AngularJS. URL: 'url', - // RESOURCE_URL is a subtype of URL used in contexts where a privileged resource is sourced from a - // url. (e.g. ng-include, script src, templateUrl) + + // RESOURCE_URL is a subtype of URL used where the referred-to resource could be interpreted as + // code. (e.g. ng-include, script src binding, templateUrl) RESOURCE_URL: 'resourceUrl', + + // Script. Currently unused in AngularJS. JS: 'js' }; // Helper functions follow. +var UNDERSCORE_LOWERCASE_REGEXP = /_([a-z])/g; + +function snakeToCamel(name) { + return name + .replace(UNDERSCORE_LOWERCASE_REGEXP, fnCamelCaseReplace); +} + function adjustMatcher(matcher) { if (matcher === 'self') { return matcher; @@ -18215,8 +18716,8 @@ function adjustMatcher(matcher) { 'Illegal sequence *** in string matcher. String: {0}', matcher); } matcher = escapeForRegexp(matcher). - replace('\\*\\*', '.*'). - replace('\\*', '[^:/.?&;]*'); + replace(/\\\*\\\*/g, '.*'). + replace(/\\\*/g, '[^:/.?&;]*'); return new RegExp('^' + matcher + '$'); } else if (isRegExp(matcher)) { // The only other type of matcher allowed is a Regexp. @@ -18251,6 +18752,16 @@ function adjustMatchers(matchers) { * `$sceDelegate` is a service that is used by the `$sce` service to provide {@link ng.$sce Strict * Contextual Escaping (SCE)} services to AngularJS. * + * For an overview of this service and the functionnality it provides in AngularJS, see the main + * page for {@link ng.$sce SCE}. The current page is targeted for developers who need to alter how + * SCE works in their application, which shouldn't be needed in most cases. + * + * <div class="alert alert-danger"> + * AngularJS strongly relies on contextual escaping for the security of bindings: disabling or + * modifying this might cause cross site scripting (XSS) vulnerabilities. For libraries owners, + * changes to this service will also influence users, so be extra careful and document your changes. + * </div> + * * 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 @@ -18271,13 +18782,19 @@ function adjustMatchers(matchers) { /** * @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} + * $sceDelegate service}, used as a delegate for {@link ng.$sce Strict Contextual Escaping (SCE)}. + * + * The `$sceDelegateProvider` allows one to get/set the whitelists and blacklists used to ensure + * that the URLs used for sourcing AngularJS templates and other script-running URLs are safe (all + * places that use the `$sce.RESOURCE_URL` context). See + * {@link ng.$sceDelegateProvider#resourceUrlWhitelist $sceDelegateProvider.resourceUrlWhitelist} + * and + * {@link ng.$sceDelegateProvider#resourceUrlBlacklist $sceDelegateProvider.resourceUrlBlacklist}, * * For the general details about this service in Angular, read the main page for {@link ng.$sce * Strict Contextual Escaping (SCE)}. @@ -18286,7 +18803,7 @@ function adjustMatchers(matchers) { * * - your app is hosted at url `http://myapp.example.com/` * - but some of your templates are hosted on other domains you control such as - * `http://srv01.assets.example.com/`, `http://srv02.assets.example.com/`, etc. + * `http://srv01.assets.example.com/`, `http://srv02.assets.example.com/`, etc. * - and you have an open redirect at `http://myapp.example.com/clickThru?...`. * * Here is what a secure configuration for this scenario might look like: @@ -18306,6 +18823,13 @@ function adjustMatchers(matchers) { * ]); * }); * ``` + * Note that an empty whitelist will block every resource URL from being loaded, and will require + * you to manually mark each one as trusted with `$sce.trustAsResourceUrl`. However, templates + * requested by {@link ng.$templateRequest $templateRequest} that are present in + * {@link ng.$templateCache $templateCache} will not go through this check. If you have a mechanism + * to populate your templates in that cache at config time, then it is a good idea to remove 'self' + * from that whitelist. This helps to mitigate the security impact of certain types of issues, like + * for instance attacker-controlled `ng-includes`. */ function $SceDelegateProvider() { @@ -18321,23 +18845,23 @@ function $SceDelegateProvider() { * @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. + * 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. * - * <div class="alert alert-warning"> - * **Note:** an empty whitelist array will block all URLs! - * </div> + * @return {Array} The currently set whitelist array. * - * @return {Array} the currently set whitelist array. + * @description + * Sets/Gets the whitelist of trusted resource URLs. * * The **default value** when no whitelist has been explicitly set is `['self']` allowing only * same origin resource requests. * - * @description - * Sets/Gets the whitelist of trusted resource URLs. + * <div class="alert alert-warning"> + * **Note:** the default whitelist of 'self' is not recommended if your app shares its origin + * with other apps! It is a good idea to limit it to only your application's directory. + * </div> */ this.resourceUrlWhitelist = function(value) { if (arguments.length) { @@ -18352,25 +18876,23 @@ function $SceDelegateProvider() { * @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. + * provided. This must be an array or null. A snapshot of this array is used so further + * changes to the array are ignored.</p><p> + * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items + * allowed in this array.</p><p> + * 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. + * </p><p> + * Finally, **the blacklist overrides the whitelist** and has the final say. + * + * @return {Array} The currently set blacklist array. * - * @return {Array} the currently set blacklist array. + * @description + * Sets/Gets the blacklist of trusted resource URLs. * * The **default value** when no whitelist has been explicitly set is the empty array (i.e. there * is no blacklist.) - * - * @description - * Sets/Gets the blacklist of trusted resource URLs. */ this.resourceUrlBlacklist = function(value) { @@ -18454,17 +18976,24 @@ function $SceDelegateProvider() { * @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. + * Returns a trusted representation of the parameter for the specified context. This trusted + * object will later on be used as-is, without any security check, by bindings or directives + * that require this security context. + * For instance, marking a string as trusted for the `$sce.HTML` context will entirely bypass + * the potential `$sanitize` call in corresponding `$sce.HTML` bindings or directives, such as + * `ng-bind-html`. Note that in most cases you won't need to call this function: if you have the + * sanitizer loaded, passing the value itself will render all the HTML that does not pose a + * security risk. + * + * See {@link ng.$sceDelegate#getTrusted getTrusted} for the function that will consume those + * trusted values, and {@link ng.$sce $sce} for general documentation about strict contextual + * escaping. * - * @param {string} type The 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. + * @param {string} type The context in which this value is safe for use, e.g. `$sce.URL`, + * `$sce.RESOURCE_URL`, `$sce.HTML`, `$sce.JS` or `$sce.CSS`. + * + * @param {*} value The value that should be considered trusted. + * @return {*} A trusted representation of value, that can be used in the given context. */ function trustAs(type, trustedValue) { var Constructor = (byType.hasOwnProperty(type) ? byType[type] : null); @@ -18496,11 +19025,11 @@ function $SceDelegateProvider() { * 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. + * ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}, it must be returned 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 + * call or anything else. + * @return {*} The `value` that was originally provided to {@link ng.$sceDelegate#trustAs * `$sceDelegate.trustAs`} if `value` is the result of such a call. Otherwise, returns * `value` unchanged. */ @@ -18517,33 +19046,38 @@ function $SceDelegateProvider() { * @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. + * Takes any input, and either returns a value that's safe to use in the specified context, or + * throws an exception. * - * <div class="alert alert-danger"> - * Disabling auto-escaping is extremely dangerous, it usually creates a Cross Site Scripting - * (XSS) vulnerability in your application. - * </div> + * In practice, there are several cases. When given a string, this function runs checks + * and sanitization to make it safe without prior assumptions. When given the result of a {@link + * ng.$sceDelegate#trustAs `$sceDelegate.trustAs`} call, it returns the originally supplied + * value if that value's context is valid for this call's context. Finally, this function can + * also throw when there is no way to turn `maybeTrusted` in a safe value (e.g., no sanitization + * is available or possible.) * - * @param {string} type The kind of context in which this value is to be used. + * @param {string} type The context in which this value is to be used (such as `$sce.HTML`). * @param {*} maybeTrusted The result of a prior {@link ng.$sceDelegate#trustAs - * `$sceDelegate.trustAs`} call. - * @returns {*} The value the was originally provided to {@link ng.$sceDelegate#trustAs - * `$sceDelegate.trustAs`} if valid in this context. Otherwise, throws an exception. + * `$sceDelegate.trustAs`} call, or anything else (which will not be considered trusted.) + * @return {*} A version of the value that's safe to use in the given context, or throws an + * exception if this is impossible. */ function getTrusted(type, maybeTrusted) { if (maybeTrusted === null || isUndefined(maybeTrusted) || maybeTrusted === '') { return maybeTrusted; } var constructor = (byType.hasOwnProperty(type) ? byType[type] : null); + // If maybeTrusted is a trusted class instance or subclass instance, then unwrap and return + // as-is. if (constructor && maybeTrusted instanceof constructor) { return maybeTrusted.$$unwrapTrustedValue(); } - // If we get here, then we may only take one of two actions. - // 1. sanitize the value for the requested type, or - // 2. throw an exception. + // Otherwise, if we get here, then we may either make it safe, or throw an exception. This + // depends on the context: some are sanitizatible (HTML), some use whitelists (RESOURCE_URL), + // some are impossible to do (JS). This step isn't implemented for CSS and URL, as AngularJS + // has no corresponding sinks. if (type === SCE_CONTEXTS.RESOURCE_URL) { + // RESOURCE_URL uses a whitelist. if (isResourceUrlAllowedByPolicy(maybeTrusted)) { return maybeTrusted; } else { @@ -18552,8 +19086,10 @@ function $SceDelegateProvider() { maybeTrusted.toString()); } } else if (type === SCE_CONTEXTS.HTML) { + // htmlSanitizer throws its own error when no sanitizer is available. return htmlSanitizer(maybeTrusted); } + // Default error when the $sce service has no way to make the input safe. throw $sceMinErr('unsafe', 'Attempting to use an unsafe value in a safe context.'); } @@ -18567,6 +19103,8 @@ function $SceDelegateProvider() { /** * @ngdoc provider * @name $sceProvider + * @this + * * @description * * The $sceProvider provider allows developers to configure the {@link ng.$sce $sce} service. @@ -18576,8 +19114,6 @@ function $SceDelegateProvider() { * Read more about {@link ng.$sce Strict Contextual Escaping (SCE)}. */ -/* jshint maxlen: false*/ - /** * @ngdoc service * @name $sce @@ -18589,21 +19125,27 @@ function $SceDelegateProvider() { * * # Strict Contextual Escaping * - * Strict Contextual Escaping (SCE) is a mode in which AngularJS requires bindings in certain - * contexts to result in a value that is marked as safe to use for that context. One example of - * such a context is binding arbitrary html controlled by the user via `ng-bind-html`. We refer - * to these contexts as privileged or SCE contexts. + * Strict Contextual Escaping (SCE) is a mode in which AngularJS constrains bindings to only render + * trusted values. Its goal is to assist in writing code in a way that (a) is secure by default, and + * (b) makes auditing for security vulnerabilities such as XSS, clickjacking, etc. a lot easier. + * + * ## Overview + * + * To systematically block XSS security bugs, AngularJS treats all values as untrusted by default in + * HTML or sensitive URL bindings. When binding untrusted values, AngularJS will automatically + * run security checks on them (sanitizations, whitelists, depending on context), or throw when it + * cannot guarantee the security of the result. That behavior depends strongly on contexts: HTML + * can be sanitized, but template URLs cannot, for instance. * - * As of version 1.2, Angular ships with SCE enabled by default. + * To illustrate this, consider the `ng-bind-html` directive. It renders its value directly as HTML: + * we call that the *context*. When given an untrusted input, AngularJS will attempt to sanitize it + * before rendering if a sanitizer is available, and throw otherwise. To bypass sanitization and + * render the input as-is, you will need to mark it as trusted for that context before attempting + * to bind it. * - * 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 - * <http://blogs.msdn.com/b/ie/archive/2008/10/16/ending-expressions.aspx> to learn more about them. - * You can ensure your document is in standards mode and not quirks mode by adding `<!doctype html>` - * to the top of your HTML document. + * As of version 1.2, AngularJS ships with SCE enabled by default. * - * 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. + * ## In practice * * Here's an example of a binding in a privileged context: * @@ -18613,10 +19155,10 @@ function $SceDelegateProvider() { * ``` * * Notice that `ng-bind-html` is bound to `userHtml` controlled by the user. With SCE - * disabled, this application allows the user to render arbitrary HTML into the DIV. - * In a more realistic example, one may be rendering user comments, blog articles, etc. via - * bindings. (HTML is just one example of a context where rendering user controlled input creates - * security vulnerabilities.) + * disabled, this application allows the user to render arbitrary HTML into the DIV, which would + * be an XSS security bug. In a more realistic example, one may be rendering user comments, blog + * articles, etc. via bindings. (HTML is just one example of a context where rendering user + * controlled input creates security vulnerabilities.) * * For the case of HTML, you might use a library, either on the client side, or on the server side, * to sanitize unsafe HTML before binding to the value and rendering it in the document. @@ -18626,25 +19168,29 @@ function $SceDelegateProvider() { * 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. + * To be secure by default, AngularJS makes sure bindings go through that sanitization, or + * any similar validation process, unless there's a good reason to trust the given value in this + * context. That trust is formalized with a function call. This means that as a developer, you + * can assume all untrusted bindings are safe. Then, to audit your code for binding security issues, + * you just need to ensure the values you mark as trusted indeed are safe - because they were + * received from your server, sanitized by your library, etc. You can organize your codebase to + * help with this - perhaps allowing only the files in a specific directory to do this. + * Ensuring that the internal API exposed by that code doesn't markup arbitrary values as safe then + * becomes a more manageable task. * * In the case of AngularJS' SCE service, one uses {@link ng.$sce#trustAs $sce.trustAs} * (and shorthand methods such as {@link ng.$sce#trustAsHtml $sce.trustAsHtml}, etc.) to - * obtain values that will be accepted by SCE / privileged contexts. - * + * build the trusted versions of your values. * * ## How does it work? * * In privileged contexts, directives and code will bind to the result of {@link ng.$sce#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. + * $sce.getTrusted(context, value)} rather than to the value directly. Think of this function as + * a way to enforce the required security context in your data sink. Directives use {@link + * ng.$sce#parseAs $sce.parseAs} rather than `$parse` to watch attribute bindings, which performs + * the {@link ng.$sce#getTrusted $sce.getTrusted} behind the scenes on non-constant literals. Also, + * when binding without directives, AngularJS will understand the context of your bindings + * automatically. * * As an example, {@link ng.directive:ngBindHtml ngBindHtml} uses {@link * ng.$sce#parseAsHtml $sce.parseAsHtml(binding expression)}. Here's the actual code (slightly @@ -18685,11 +19231,12 @@ function $SceDelegateProvider() { * 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. - * `<div ng-bind-html="'<b>implicitly trusted</b>'"></div>`) 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. + * call `$sce.trustAs` on them (e.g. + * `<div ng-bind-html="'<b>implicitly trusted</b>'"></div>`) just works. The `$sceDelegate` will + * also use the `$sanitize` service if it is available when binding untrusted values to + * `$sce.HTML` context. AngularJS provides an implementation in `angular-sanitize.js`, and if you + * wish to use it, you will also need to depend on the {@link ngSanitize `ngSanitize`} module in + * your application. * * The included {@link ng.$sceDelegate $sceDelegate} comes with sane defaults to allow you to load * templates in `ng-include` from your application's domain without having to even know about SCE. @@ -18707,11 +19254,17 @@ function $SceDelegateProvider() { * * | 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 (`<a href=` and `<img src=` sanitize their urls and don't constitute an SCE context. | - * | `$sce.RESOURCE_URL` | For URLs that are not only safe to follow as links, but whose contents are also safe to include in your application. Examples include `ng-include`, `src` / `ngSrc` bindings for tags other than `IMG` (e.g. `IFRAME`, `OBJECT`, etc.) <br><br>Note that `$sce.RESOURCE_URL` makes a stronger statement about the URL than `$sce.URL` does and therefore contexts requiring values trusted for `$sce.RESOURCE_URL` can be used anywhere that values trusted for `$sce.URL` are required. | - * | `$sce.JS` | For JavaScript that is safe to execute in your application's context. Currently unused. Feel free to use it in your own directives. | + * | `$sce.HTML` | For HTML that's safe to source into the application. The {@link ng.directive:ngBindHtml ngBindHtml} directive uses this context for bindings. If an unsafe value is encountered, and the {@link ngSanitize.$sanitize $sanitize} service is available (implemented by the {@link ngSanitize ngSanitize} module) this will sanitize the value instead of throwing an error. | + * | `$sce.CSS` | For CSS that's safe to source into the application. Currently, no bindings require this context. Feel free to use it in your own directives. | + * | `$sce.URL` | For URLs that are safe to follow as links. Currently unused (`<a href=`, `<img src=`, and some others sanitize their urls and don't constitute an SCE context.) | + * | `$sce.RESOURCE_URL` | For URLs that are not only safe to follow as links, but whose contents are also safe to include in your application. Examples include `ng-include`, `src` / `ngSrc` bindings for tags other than `IMG`, `VIDEO`, `AUDIO`, `SOURCE`, and `TRACK` (e.g. `IFRAME`, `OBJECT`, etc.) <br><br>Note that `$sce.RESOURCE_URL` makes a stronger statement about the URL than `$sce.URL` does (it's not just the URL that matters, but also what is at the end of it), and therefore contexts requiring values trusted for `$sce.RESOURCE_URL` can be used anywhere that values trusted for `$sce.URL` are required. | + * | `$sce.JS` | For JavaScript that is safe to execute in your application's context. Currently, no bindings require this context. Feel free to use it in your own directives. | + * + * + * Be aware that `a[href]` and `img[src]` automatically sanitize their URLs and do not pass them + * through {@link ng.$sce#getTrusted $sce.getTrusted}. There's no CSS-, URL-, or JS-context bindings + * in AngularJS currently, so their corresponding `$sce.trustAs` functions aren't useful yet. This + * might evolve. * * ## Format of items in {@link ng.$sceDelegateProvider#resourceUrlWhitelist resourceUrlWhitelist}/{@link ng.$sceDelegateProvider#resourceUrlBlacklist Blacklist} <a name="resourceUrlPatternItem"></a> * @@ -18762,7 +19315,7 @@ function $SceDelegateProvider() { * * ## Show me an example using SCE. * - * <example module="mySceApp" deps="angular-sanitize.js"> + * <example module="mySceApp" deps="angular-sanitize.js" name="sce-service"> * <file name="index.html"> * <div ng-controller="AppController as myCtrl"> * <i ng-bind-html="myCtrl.explicitlyTrustedHtml" id="explicitlyTrustedHtml"></i><br><br> @@ -18783,10 +19336,10 @@ function $SceDelegateProvider() { * <file name="script.js"> * angular.module('mySceApp', ['ngSanitize']) * .controller('AppController', ['$http', '$templateCache', '$sce', - * function($http, $templateCache, $sce) { + * function AppController($http, $templateCache, $sce) { * var self = this; - * $http.get("test_data.json", {cache: $templateCache}).success(function(userComments) { - * self.userComments = userComments; + * $http.get('test_data.json', {cache: $templateCache}).then(function(response) { + * self.userComments = response.data; * }); * self.explicitlyTrustedHtml = $sce.trustAsHtml( * '<span onmouseover="this.textContent="Explicitly trusted HTML bypasses ' + @@ -18809,12 +19362,12 @@ function $SceDelegateProvider() { * <file name="protractor.js" type="protractor"> * describe('SCE doc demo', function() { * it('should sanitize untrusted values', function() { - * expect(element.all(by.css('.htmlComment')).first().getInnerHtml()) + * expect(element.all(by.css('.htmlComment')).first().getAttribute('innerHTML')) * .toBe('<span>Is <i>anyone</i> reading this?</span>'); * }); * * it('should NOT sanitize explicitly trusted values', function() { - * expect(element(by.id('explicitlyTrustedHtml')).getInnerHtml()).toBe( + * expect(element(by.id('explicitlyTrustedHtml')).getAttribute('innerHTML')).toBe( * '<span onmouseover="this.textContent="Explicitly trusted HTML bypasses ' + * 'sanitization."">Hover over this text.</span>'); * }); @@ -18830,20 +19383,20 @@ function $SceDelegateProvider() { * for little coding overhead. It will be much harder to take an SCE disabled application and * either secure it on your own or enable SCE at a later stage. It might make sense to disable SCE * for cases where you have a lot of existing code that was written before SCE was introduced and - * you're migrating them a module at a time. + * you're migrating them a module at a time. Also do note that this is an app-wide setting, so if + * you are writing a library, you will cause security bugs applications using it. * * That said, here's how you can completely disable SCE: * * ``` * angular.module('myAppWithSceDisabledmyApp', []).config(function($sceProvider) { * // Completely disable SCE. For demonstration purposes only! - * // Do not use in new projects. + * // Do not use in new projects or libraries. * $sceProvider.enabled(false); * }); * ``` * */ -/* jshint maxlen: 100 */ function $SceProvider() { var enabled = true; @@ -18853,8 +19406,8 @@ function $SceProvider() { * @name $sceProvider#enabled * @kind function * - * @param {boolean=} value If provided, then enables/disables SCE. - * @return {boolean} true if SCE is enabled, false otherwise. + * @param {boolean=} value If provided, then enables/disables SCE application-wide. + * @return {boolean} True if SCE is enabled, false otherwise. * * @description * Enables/disables SCE and returns the current value. @@ -18908,13 +19461,14 @@ function $SceProvider() { * 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. + * 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) { + // Support: IE 9-11 only // Prereq: Ensure that we're not running in IE<11 quirks mode. In that mode, IE < 11 allow // the "expression(javascript expression)" syntax which is insecure. if (enabled && msie < 8) { @@ -18931,8 +19485,8 @@ function $SceProvider() { * @name $sce#isEnabled * @kind function * - * @return {Boolean} true if SCE is enabled, false otherwise. If you want to set the value, you - * have to do it at module config time on {@link ng.$sceProvider $sceProvider}. + * @return {Boolean} True if SCE is enabled, false otherwise. If you want to set the value, you + * have to do it at module config time on {@link ng.$sceProvider $sceProvider}. * * @description * Returns a boolean indicating if SCE is enabled. @@ -18959,14 +19513,14 @@ function $SceProvider() { * wraps the expression in a call to {@link ng.$sce#getTrusted $sce.getTrusted(*type*, * *result*)} * - * @param {string} type The kind of SCE context in which this result will be used. + * @param {string} type The SCE context in which this result will be used. * @param {string} expression String expression to compile. - * @returns {function(context, locals)} a function which represents the compiled expression: + * @return {function(context, locals)} A function which represents the compiled expression: * - * * `context` – `{object}` – an object against which any expressions embedded in the strings - * are evaluated against (typically a scope object). - * * `locals` – `{object=}` – local variables context object, useful for overriding values in - * `context`. + * * `context` – `{object}` – an object against which any expressions embedded in the + * strings are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values + * in `context`. */ sce.parseAs = function sceParseAs(type, expr) { var parsed = $parse(expr); @@ -18984,18 +19538,18 @@ function $SceProvider() { * @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. + * Delegates to {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}. As such, returns a + * wrapped object that represents your value, and the trust you have in its safety for the given + * context. AngularJS can then use that value as-is in bindings of the specified secure context. + * This is used in bindings for `ng-bind-html`, `ng-include`, and most `src` attribute + * interpolations. See {@link ng.$sce $sce} for strict contextual escaping. + * + * @param {string} type The context in which this value is safe for use, e.g. `$sce.URL`, + * `$sce.RESOURCE_URL`, `$sce.HTML`, `$sce.JS` or `$sce.CSS`. * - * @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. + * @param {*} value The value that that should be considered trusted. + * @return {*} A wrapped version of value that can be used as a trusted variant of your `value` + * in the context you specified. */ /** @@ -19006,11 +19560,23 @@ function $SceProvider() { * 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}.) + * @param {*} value The value to mark as trusted for `$sce.HTML` context. + * @return {*} A wrapped version of value that can be used as a trusted variant of your `value` + * in `$sce.HTML` context (like `ng-bind-html`). + */ + + /** + * @ngdoc method + * @name $sce#trustAsCss + * + * @description + * Shorthand method. `$sce.trustAsCss(value)` → + * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.CSS, value)`} + * + * @param {*} value The value to mark as trusted for `$sce.CSS` context. + * @return {*} A wrapped version of value that can be used as a trusted variant + * of your `value` in `$sce.CSS` context. This context is currently unused, so there are + * almost no reasons to use this function so far. */ /** @@ -19021,11 +19587,10 @@ function $SceProvider() { * 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}.) + * @param {*} value The value to mark as trusted for `$sce.URL` context. + * @return {*} A wrapped version of value that can be used as a trusted variant of your `value` + * in `$sce.URL` context. That context is currently unused, so there are almost no reasons + * to use this function so far. */ /** @@ -19036,11 +19601,10 @@ function $SceProvider() { * 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}.) + * @param {*} value The value to mark as trusted for `$sce.RESOURCE_URL` context. + * @return {*} A wrapped version of value that can be used as a trusted variant of your `value` + * in `$sce.RESOURCE_URL` context (template URLs in `ng-include`, most `src` attribute + * bindings, ...) */ /** @@ -19051,11 +19615,10 @@ function $SceProvider() { * 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}.) + * @param {*} value The value to mark as trusted for `$sce.JS` context. + * @return {*} A wrapped version of value that can be used as a trusted variant of your `value` + * in `$sce.JS` context. That context is currently unused, so there are almost no reasons to + * use this function so far. */ /** @@ -19064,16 +19627,17 @@ function $SceProvider() { * * @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. + * takes any input, and either returns a value that's safe to use in the specified context, + * or throws an exception. This function is aware of trusted values created by the `trustAs` + * function and its shorthands, and when contexts are appropriate, returns the unwrapped value + * as-is. Finally, this function can also throw when there is no way to turn `maybeTrusted` in a + * safe value (e.g., no sanitization is available or possible.) * - * @param {string} type The kind of context in which this value is to be used. - * @param {*} maybeTrusted The result of a prior {@link ng.$sce#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. + * @param {string} type The context in which this value is to be used. + * @param {*} maybeTrusted The result of a prior {@link ng.$sce#trustAs + * `$sce.trustAs`} call, or anything else (which will not be considered trusted.) + * @return {*} A version of the value that's safe to use in the given context, or throws an + * exception if this is impossible. */ /** @@ -19085,7 +19649,7 @@ function $SceProvider() { * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.HTML, value)`} * * @param {*} value The value to pass to `$sce.getTrusted`. - * @returns {*} The return value of `$sce.getTrusted($sce.HTML, value)` + * @return {*} The return value of `$sce.getTrusted($sce.HTML, value)` */ /** @@ -19097,7 +19661,7 @@ function $SceProvider() { * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.CSS, value)`} * * @param {*} value The value to pass to `$sce.getTrusted`. - * @returns {*} The return value of `$sce.getTrusted($sce.CSS, value)` + * @return {*} The return value of `$sce.getTrusted($sce.CSS, value)` */ /** @@ -19109,7 +19673,7 @@ function $SceProvider() { * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.URL, value)`} * * @param {*} value The value to pass to `$sce.getTrusted`. - * @returns {*} The return value of `$sce.getTrusted($sce.URL, value)` + * @return {*} The return value of `$sce.getTrusted($sce.URL, value)` */ /** @@ -19121,7 +19685,7 @@ function $SceProvider() { * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.RESOURCE_URL, value)`} * * @param {*} value The value to pass to `$sceDelegate.getTrusted`. - * @returns {*} The return value of `$sce.getTrusted($sce.RESOURCE_URL, value)` + * @return {*} The return value of `$sce.getTrusted($sce.RESOURCE_URL, value)` */ /** @@ -19133,7 +19697,7 @@ function $SceProvider() { * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.JS, value)`} * * @param {*} value The value to pass to `$sce.getTrusted`. - * @returns {*} The return value of `$sce.getTrusted($sce.JS, value)` + * @return {*} The return value of `$sce.getTrusted($sce.JS, value)` */ /** @@ -19145,12 +19709,12 @@ function $SceProvider() { * {@link ng.$sce#parseAs `$sce.parseAs($sce.HTML, value)`} * * @param {string} expression String expression to compile. - * @returns {function(context, locals)} a function which represents the compiled expression: + * @return {function(context, locals)} A function which represents the compiled expression: * - * * `context` – `{object}` – an object against which any expressions embedded in the strings - * are evaluated against (typically a scope object). - * * `locals` – `{object=}` – local variables context object, useful for overriding values in - * `context`. + * * `context` – `{object}` – an object against which any expressions embedded in the + * strings are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values + * in `context`. */ /** @@ -19162,12 +19726,12 @@ function $SceProvider() { * {@link ng.$sce#parseAs `$sce.parseAs($sce.CSS, value)`} * * @param {string} expression String expression to compile. - * @returns {function(context, locals)} a function which represents the compiled expression: + * @return {function(context, locals)} A function which represents the compiled expression: * - * * `context` – `{object}` – an object against which any expressions embedded in the strings - * are evaluated against (typically a scope object). - * * `locals` – `{object=}` – local variables context object, useful for overriding values in - * `context`. + * * `context` – `{object}` – an object against which any expressions embedded in the + * strings are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values + * in `context`. */ /** @@ -19179,12 +19743,12 @@ function $SceProvider() { * {@link ng.$sce#parseAs `$sce.parseAs($sce.URL, value)`} * * @param {string} expression String expression to compile. - * @returns {function(context, locals)} a function which represents the compiled expression: + * @return {function(context, locals)} A function which represents the compiled expression: * - * * `context` – `{object}` – an object against which any expressions embedded in the strings - * are evaluated against (typically a scope object). - * * `locals` – `{object=}` – local variables context object, useful for overriding values in - * `context`. + * * `context` – `{object}` – an object against which any expressions embedded in the + * strings are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values + * in `context`. */ /** @@ -19196,12 +19760,12 @@ function $SceProvider() { * {@link ng.$sce#parseAs `$sce.parseAs($sce.RESOURCE_URL, value)`} * * @param {string} expression String expression to compile. - * @returns {function(context, locals)} a function which represents the compiled expression: + * @return {function(context, locals)} A function which represents the compiled expression: * - * * `context` – `{object}` – an object against which any expressions embedded in the strings - * are evaluated against (typically a scope object). - * * `locals` – `{object=}` – local variables context object, useful for overriding values in - * `context`. + * * `context` – `{object}` – an object against which any expressions embedded in the + * strings are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values + * in `context`. */ /** @@ -19213,12 +19777,12 @@ function $SceProvider() { * {@link ng.$sce#parseAs `$sce.parseAs($sce.JS, value)`} * * @param {string} expression String expression to compile. - * @returns {function(context, locals)} a function which represents the compiled expression: + * @return {function(context, locals)} A function which represents the compiled expression: * - * * `context` – `{object}` – an object against which any expressions embedded in the strings - * are evaluated against (typically a scope object). - * * `locals` – `{object=}` – local variables context object, useful for overriding values in - * `context`. + * * `context` – `{object}` – an object against which any expressions embedded in the + * strings are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values + * in `context`. */ // Shorthand delegations. @@ -19228,13 +19792,13 @@ function $SceProvider() { forEach(SCE_CONTEXTS, function(enumValue, name) { var lName = lowercase(name); - sce[camelCase("parse_as_" + lName)] = function(expr) { + sce[snakeToCamel('parse_as_' + lName)] = function(expr) { return parse(enumValue, expr); }; - sce[camelCase("get_trusted_" + lName)] = function(value) { + sce[snakeToCamel('get_trusted_' + lName)] = function(value) { return getTrusted(enumValue, value); }; - sce[camelCase("trust_as_" + lName)] = function(value) { + sce[snakeToCamel('trust_as_' + lName)] = function(value) { return trustAs(enumValue, value); }; }); @@ -19243,12 +19807,15 @@ function $SceProvider() { }]; } +/* 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 ? @@ -19260,41 +19827,32 @@ function $SceProvider() { function $SnifferProvider() { this.$get = ['$window', '$document', function($window, $document) { var eventSupport = {}, - // Chrome Packaged Apps are not allowed to access `history.pushState`. They can be detected by - // the presence of `chrome.app.runtime` (see https://developer.chrome.com/apps/api_index) - isChromePackagedApp = $window.chrome && $window.chrome.app && $window.chrome.app.runtime, + // Chrome Packaged Apps are not allowed to access `history.pushState`. + // If not sandboxed, they can be detected by the presence of `chrome.app.runtime` + // (see https://developer.chrome.com/apps/api_index). If sandboxed, they can be detected by + // the presence of an extension runtime ID and the absence of other Chrome runtime APIs + // (see https://developer.chrome.com/apps/manifest/sandbox). + // (NW.js apps have access to Chrome APIs, but do support `history`.) + isNw = $window.nw && $window.nw.process, + isChromePackagedApp = + !isNw && + $window.chrome && + ($window.chrome.app && $window.chrome.app.runtime || + !$window.chrome.app && $window.chrome.runtime && $window.chrome.runtime.id), hasHistoryPushState = !isChromePackagedApp && $window.history && $window.history.pushState, android = toInt((/android (\d+)/.exec(lowercase(($window.navigator || {}).userAgent)) || [])[1]), boxee = /Boxee/i.test(($window.navigator || {}).userAgent), document = $document[0] || {}, - vendorPrefix, - vendorRegex = /^(Moz|webkit|ms)(?=[A-Z])/, bodyStyle = document.body && document.body.style, transitions = false, - animations = false, - match; + animations = false; 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); - } + // Support: Android <5, Blackberry Browser 10, default Chrome in Android 4.4.x + // Mentioned browsers need a -webkit- prefix for transitions & animations. + transitions = !!('transition' in bodyStyle || 'webkitTransition' in bodyStyle); + animations = !!('animation' in bodyStyle || 'webkitAnimation' in bodyStyle); } @@ -19307,16 +19865,15 @@ function $SnifferProvider() { // older webkit browser (533.9) on Boxee box has exactly the same problem as Android has // so let's not use the history API also // We are purposefully using `!(android < 4)` to cover the case when `android` is undefined - // jshint -W018 history: !!(hasHistoryPushState && !(android < 4) && !boxee), - // jshint +W018 hasEvent: function(event) { + // Support: IE 9-11 only // IE9 implements 'input' event it's so fubared that we rather pretend that it doesn't have // it. In particular the event is not fired when backspace or delete key are pressed or // when cut operation is performed. // 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 (event === 'input' && msie) return false; if (isUndefined(eventSupport[event])) { var divElm = document.createElement('div'); @@ -19326,7 +19883,6 @@ function $SnifferProvider() { return eventSupport[event]; }, csp: csp(), - vendorPrefix: vendorPrefix, transitions: transitions, animations: animations, android: android @@ -19339,6 +19895,8 @@ 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. * @@ -19392,57 +19950,64 @@ function $TemplateRequestProvider() { * * @property {number} totalPendingRequests total amount of pending template requests being downloaded. */ - this.$get = ['$templateCache', '$http', '$q', '$sce', function($templateCache, $http, $q, $sce) { + this.$get = ['$exceptionHandler', '$templateCache', '$http', '$q', '$sce', + function($exceptionHandler, $templateCache, $http, $q, $sce) { - function handleRequestFn(tpl, ignoreRequestError) { - handleRequestFn.totalPendingRequests++; + 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); - } + // 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; + 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); + if (isArray(transformResponse)) { + transformResponse = transformResponse.filter(function(transformer) { + return transformer !== defaultHttpResponseTransform; + }); + } else if (transformResponse === defaultHttpResponseTransform) { + transformResponse = null; + } + + return $http.get(tpl, extend({ + cache: $templateCache, + transformResponse: transformResponse + }, httpOptions)) + .finally(function() { + handleRequestFn.totalPendingRequests--; + }) + .then(function(response) { + $templateCache.put(tpl, response.data); + return response.data; + }, handleError); + + function handleError(resp) { + if (!ignoreRequestError) { + resp = $templateRequestMinErr('tpload', + 'Failed to load template: {0} (HTTP status: {1} {2})', + tpl, resp.status, resp.statusText); + + $exceptionHandler(resp); + } - 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); } - return $q.reject(resp); } - } - handleRequestFn.totalPendingRequests = 0; + handleRequestFn.totalPendingRequests = 0; - return handleRequestFn; - }]; + return handleRequestFn; + } + ]; } +/** @this */ function $$TestabilityProvider() { this.$get = ['$rootScope', '$browser', '$location', function($rootScope, $browser, $location) { @@ -19481,7 +20046,7 @@ function $$TestabilityProvider() { matches.push(binding); } } else { - if (bindingName.indexOf(expression) != -1) { + if (bindingName.indexOf(expression) !== -1) { matches.push(binding); } } @@ -19558,6 +20123,7 @@ function $$TestabilityProvider() { }]; } +/** @this */ function $TimeoutProvider() { this.$get = ['$rootScope', '$browser', '$q', '$$q', '$exceptionHandler', function($rootScope, $browser, $q, $$q, $exceptionHandler) { @@ -19613,8 +20179,7 @@ function $TimeoutProvider() { } catch (e) { deferred.reject(e); $exceptionHandler(e); - } - finally { + } finally { delete deferreds[promise.$$timeoutId]; } @@ -19642,6 +20207,8 @@ function $TimeoutProvider() { */ timeout.cancel = function(promise) { if (promise && promise.$$timeoutId in deferreds) { + // Timeout cancels should not report an unhandled promise. + deferreds[promise.$$timeoutId].promise.catch(noop); deferreds[promise.$$timeoutId].reject('canceled'); delete deferreds[promise.$$timeoutId]; return $browser.defer.cancel(promise.$$timeoutId); @@ -19660,7 +20227,7 @@ function $TimeoutProvider() { // 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 urlParsingNode = window.document.createElement('a'); var originUrl = urlResolve(window.location.href); @@ -19673,7 +20240,7 @@ var originUrl = urlResolve(window.location.href); * 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 + * compatibility - Safari 1+, Mozilla 1+ etc. See * http://www.aptana.com/reference/html/api/HTMLAnchorElement.html * * Implementation Notes for IE @@ -19712,10 +20279,11 @@ var originUrl = urlResolve(window.location.href); function urlResolve(url) { var href = url; + // Support: IE 9-11 only if (msie) { // Normalize before parse. Refer Implementation Notes on why this is // done in two steps on IE. - urlParsingNode.setAttribute("href", href); + urlParsingNode.setAttribute('href', href); href = urlParsingNode.href; } @@ -19752,6 +20320,7 @@ function urlIsSameOrigin(requestUrl) { /** * @ngdoc service * @name $window + * @this * * @description * A reference to the browser's `window` object. While `window` @@ -19765,7 +20334,7 @@ function urlIsSameOrigin(requestUrl) { * expression. * * @example - <example module="windowExample"> + <example module="windowExample" name="window-service"> <file name="index.html"> <script> angular.module('windowExample', []) @@ -19808,6 +20377,14 @@ function $$CookieReader($document) { var lastCookies = {}; var lastCookieString = ''; + function safeGetCookie(rawDocument) { + try { + return rawDocument.cookie || ''; + } catch (e) { + return ''; + } + } + function safeDecodeURIComponent(str) { try { return decodeURIComponent(str); @@ -19818,7 +20395,7 @@ function $$CookieReader($document) { return function() { var cookieArray, cookie, i, index, name; - var currentCookieString = rawDocument.cookie || ''; + var currentCookieString = safeGetCookie(rawDocument); if (currentCookieString !== lastCookieString) { lastCookieString = currentCookieString; @@ -19845,6 +20422,7 @@ function $$CookieReader($document) { $$CookieReader.$inject = ['$document']; +/** @this */ function $$CookieReaderProvider() { this.$get = $$CookieReader; } @@ -19924,9 +20502,15 @@ function $$CookieReaderProvider() { * @description * Filters are used for formatting data displayed to the user. * + * They can be used in view templates, controllers or services.Angular comes + * with a collection of [built-in filters](api/ng/filter), but it is easy to + * define your own as well. + * * The general syntax in templates is as follows: * - * {{ expression [| filter_name[:parameter_value] ... ] }} + * ```html + * {{ expression [| filter_name[:parameter_value] ... ] }} + * ``` * * @param {String} name Name of the filter function to retrieve * @return {Function} the filter function @@ -19949,6 +20533,7 @@ function $$CookieReaderProvider() { </example> */ $FilterProvider.$inject = ['$provide']; +/** @this */ function $FilterProvider($provide) { var suffix = 'Filter'; @@ -19998,7 +20583,7 @@ function $FilterProvider($provide) { lowercaseFilter: false, numberFilter: false, orderByFilter: false, - uppercaseFilter: false, + uppercaseFilter: false */ register('currency', currencyFilter); @@ -20021,6 +20606,9 @@ function $FilterProvider($provide) { * Selects a subset of items from `array` and returns it as a new array. * * @param {Array} array The source array. + * <div class="alert alert-info"> + * **Note**: If the array contains objects that reference themselves, filtering is not possible. + * </div> * @param {string|Object|function()} expression The predicate to be used for selecting items from * `array`. * @@ -20053,9 +20641,10 @@ function $FilterProvider($provide) { * * The final result is an array of those elements that the predicate returned true for. * - * @param {function(actual, expected)|true|undefined} comparator Comparator which is used in - * determining if the expected value (from the filter expression) and actual value (from - * the object in the array) should be considered a match. + * @param {function(actual, expected)|true|false} [comparator] Comparator which is used in + * determining if values retrieved using `expression` (when it is not a function) should be + * considered a match based on the the expected value (from the filter expression) and actual + * value (from the object in the array). * * Can be one of: * @@ -20066,17 +20655,18 @@ function $FilterProvider($provide) { * - `true`: A shorthand for `function(actual, expected) { return angular.equals(actual, expected)}`. * This is essentially strict comparison of expected and actual. * - * - `false|undefined`: A short hand for a function which will look for a substring match in case - * insensitive way. + * - `false`: A short hand for a function which will look for a substring match in a case + * insensitive way. Primitive values are converted to strings. Objects are not compared against + * primitives, unless they have a custom `toString` method (e.g. `Date` objects). + * * - * 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. + * @param {string} [anyPropertyKey] The special property name that matches against any property. * By default `$`. * * @example - <example> + <example name="filter-filter"> <file name="index.html"> <div ng-init="friends = [{name:'John', phone:'555-1276'}, {name:'Mary', phone:'800-BIG-MARY'}, @@ -20168,9 +20758,8 @@ function filterFilter() { case 'number': case 'string': matchAgainstAnyProp = true; - //jshint -W086 + // falls through case 'object': - //jshint +W086 predicateFn = createPredicateFn(expression, comparator, anyPropertyKey, matchAgainstAnyProp); break; default: @@ -20238,7 +20827,10 @@ function deepCompare(actual, expected, comparator, anyPropertyKey, matchAgainstA var key; if (matchAgainstAnyProp) { for (key in actual) { - if ((key.charAt(0) !== '$') && deepCompare(actual[key], expected, comparator, anyPropertyKey, true)) { + // Under certain, rare, circumstances, key may not be a string and `charAt` will be undefined + // See: https://github.com/angular/angular.js/issues/15644 + if (key.charAt && (key.charAt(0) !== '$') && + deepCompare(actual[key], expected, comparator, anyPropertyKey, true)) { return true; } } @@ -20260,7 +20852,6 @@ function deepCompare(actual, expected, comparator, anyPropertyKey, matchAgainstA } else { return comparator(actual, expected); } - break; case 'function': return false; default: @@ -20293,7 +20884,7 @@ var ZERO_CHAR = '0'; * * * @example - <example module="currencyExample"> + <example module="currencyExample" name="currency-filter"> <file name="index.html"> <script> angular.module('currencyExample', []) @@ -20304,7 +20895,7 @@ var ZERO_CHAR = '0'; <div ng-controller="ExampleController"> <input type="number" ng-model="amount" aria-label="amount"> <br> default currency symbol ($): <span id="currency-default">{{amount | currency}}</span><br> - custom currency identifier (USD$): <span id="currency-custom">{{amount | currency:"USD$"}}</span> + custom currency identifier (USD$): <span id="currency-custom">{{amount | currency:"USD$"}}</span><br> no fractions (0): <span id="currency-no-fractions">{{amount | currency:"USD$":0}}</span> </div> </file> @@ -20315,7 +20906,7 @@ var ZERO_CHAR = '0'; expect(element(by.id('currency-no-fractions')).getText()).toBe('USD$1,235'); }); it('should update', function() { - if (browser.params.browser == 'safari') { + if (browser.params.browser === 'safari') { // Safari does not understand the minus key. See // https://github.com/angular/protractor/issues/481 return; @@ -20371,7 +20962,7 @@ function currencyFilter($locale) { * include "," group separators after each third digit). * * @example - <example module="numberFilterExample"> + <example module="numberFilterExample" name="number-filter"> <file name="index.html"> <script> angular.module('numberFilterExample', []) @@ -20450,16 +21041,16 @@ function parse(numStr) { } // Count the number of leading zeros. - for (i = 0; numStr.charAt(i) == ZERO_CHAR; i++) {/* jshint noempty: false */} + for (i = 0; numStr.charAt(i) === ZERO_CHAR; i++) { /* empty */ } - if (i == (zeros = numStr.length)) { + 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--; + while (numStr.charAt(zeros) === ZERO_CHAR) zeros--; // Trailing zeros are insignificant so ignore them numberOfIntegerDigits -= i; @@ -20651,7 +21242,7 @@ function dateGetter(name, size, offset, trim, negWrap) { if (offset > 0 || value > -offset) { value += offset; } - if (value === 0 && offset == -12) value = 12; + if (value === 0 && offset === -12) value = 12; return padNumber(value, size, trim, negWrap); }; } @@ -20668,7 +21259,7 @@ function dateStrGetter(name, shortForm, standAlone) { function timeZoneGetter(date, formats, offset) { var zone = -1 * offset; - var paddedZone = (zone >= 0) ? "+" : ""; + var paddedZone = (zone >= 0) ? '+' : ''; paddedZone += padNumber(Math[zone > 0 ? 'floor' : 'ceil'](zone / 60), 2) + padNumber(Math.abs(zone % 60), 2); @@ -20748,8 +21339,8 @@ var DATE_FORMATS = { GGGG: longEraGetter }; -var DATE_FORMATS_SPLIT = /((?:[^yMLdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|L+|d+|H+|h+|m+|s+|a|Z|G+|w+))(.*)/, - NUMBER_STRING = /^\-?\d+$/; +var DATE_FORMATS_SPLIT = /((?:[^yMLdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|L+|d+|H+|h+|m+|s+|a|Z|G+|w+))([\s\S]*)/, + NUMBER_STRING = /^-?\d+$/; /** * @ngdoc filter @@ -20807,6 +21398,8 @@ var DATE_FORMATS_SPLIT = /((?:[^yMLdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+ * `"h 'in the morning'"`). In order to output a single quote, escape it - i.e., two single quotes in a sequence * (e.g. `"h 'o''clock'"`). * + * Any other characters in the `format` string will be output as-is. + * * @param {(Date|number|string)} date Date to format either as Date object, milliseconds (string or * number) or various ISO 8601 datetime string formats (e.g. yyyy-MM-ddTHH:mm:ss.sssZ and its * shorter versions like yyyy-MM-ddTHH:mmZ, yyyy-MM-dd or yyyyMMddTHHmmssZ). If no timezone is @@ -20820,7 +21413,7 @@ var DATE_FORMATS_SPLIT = /((?:[^yMLdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+ * @returns {string} Formatted string or the input if input is not recognized as date/millis. * * @example - <example> + <example name="filter-date"> <file name="index.html"> <span ng-non-bindable>{{1288323623006 | date:'medium'}}</span>: <span>{{1288323623006 | date:'medium'}}</span><br> @@ -20836,7 +21429,7 @@ var DATE_FORMATS_SPLIT = /((?:[^yMLdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+ 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}/); + 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()). @@ -20853,7 +21446,7 @@ function dateFilter($locale) { // 1 2 3 4 5 6 7 8 9 10 11 function jsonStringToDate(string) { var match; - if (match = string.match(R_ISO8601_STR)) { + if ((match = string.match(R_ISO8601_STR))) { var date = new Date(0), tzHour = 0, tzMin = 0, @@ -20914,7 +21507,7 @@ function dateFilter($locale) { forEach(parts, function(value) { fn = DATE_FORMATS[value]; text += fn ? fn(date, $locale.DATETIME_FORMATS, dateTimezoneOffset) - : value === "''" ? "'" : value.replace(/(^'|'$)/g, '').replace(/''/g, "'"); + : value === '\'\'' ? '\'' : value.replace(/(^'|'$)/g, '').replace(/''/g, '\''); }); return text; @@ -20939,15 +21532,15 @@ function dateFilter($locale) { * * * @example - <example> + <example name="filter-json"> <file name="index.html"> <pre id="default-spacing">{{ {'name':'value'} | json }}</pre> <pre id="custom-spacing">{{ {'name':'value'} | json:4 }}</pre> </file> <file name="protractor.js" type="protractor"> it('should jsonify filtered objects', function() { - expect(element(by.id('default-spacing')).getText()).toMatch(/\{\n "name": ?"value"\n}/); - expect(element(by.id('custom-spacing')).getText()).toMatch(/\{\n "name": ?"value"\n}/); + expect(element(by.id('default-spacing')).getText()).toMatch(/\{\n {2}"name": ?"value"\n}/); + expect(element(by.id('custom-spacing')).getText()).toMatch(/\{\n {4}"name": ?"value"\n}/); }); </file> </example> @@ -21008,7 +21601,7 @@ var uppercaseFilter = valueFn(uppercase); * less than `limit` elements. * * @example - <example module="limitToExample"> + <example module="limitToExample" name="limit-to-filter"> <file name="index.html"> <script> angular.module('limitToExample', []) @@ -21090,7 +21683,7 @@ function limitToFilter() { } else { limit = toInt(limit); } - if (isNaN(limit)) return input; + if (isNumberNaN(limit)) return input; if (isNumber(input)) input = input.toString(); if (!isArrayLike(input)) return input; @@ -21132,7 +21725,7 @@ function sliceFn(input, begin, end) { * String, etc). * * The `expression` can be a single predicate, or a list of predicates each serving as a tie-breaker - * for the preceeding one. The `expression` is evaluated against each item and the output is used + * 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 @@ -21202,6 +21795,9 @@ function sliceFn(input, begin, end) { * * **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.<Function|string>)=} expression - A predicate (or list of @@ -21723,8 +22319,8 @@ function orderByFilter($parse) { if (isFunction(predicate)) { get = predicate; } else if (isString(predicate)) { - if ((predicate.charAt(0) == '+' || predicate.charAt(0) == '-')) { - descending = predicate.charAt(0) == '-' ? -1 : 1; + if ((predicate.charAt(0) === '+' || predicate.charAt(0) === '-')) { + descending = predicate.charAt(0) === '-' ? -1 : 1; predicate = predicate.substring(1); } if (predicate !== '') { @@ -21823,12 +22419,10 @@ function ngDirective(directive) { * @restrict E * * @description - * Modifies the default behavior of the html A tag so that the default action is prevented when + * Modifies the default behavior of the html a tag so that the default action is prevented when * the href attribute is empty. * - * This change permits the easy creation of action links with the `ngClick` directive - * without changing the location or causing page reloads, e.g.: - * `<a href="" ng-click="list.addItem()">Add Item</a>` + * For dynamically creating `href` attributes for a tags, see the {@link ng.ngHref `ngHref`} directive. */ var htmlAnchorDirective = valueFn({ restrict: 'E', @@ -21882,7 +22476,7 @@ var htmlAnchorDirective = valueFn({ * @example * This example shows various combinations of `href`, `ng-href` and `ng-click` attributes * in links and their different behaviors: - <example> + <example name="ng-href"> <file name="index.html"> <input ng-model="value" /><br /> <a id="link-1" href ng-click="value = 1">link 1</a> (link, don't reload)<br /> @@ -22011,14 +22605,15 @@ var htmlAnchorDirective = valueFn({ * * @description * - * This directive sets the `disabled` attribute on the element if the + * This directive sets the `disabled` attribute on the element (typically a form control, + * e.g. `input`, `button`, `select` etc.) if the * {@link guide/expression expression} inside `ngDisabled` evaluates to truthy. * * 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 - <example> + <example name="ng-disabled"> <file name="index.html"> <label>Click me to toggle: <input type="checkbox" ng-model="checked"></label><br/> <button ng-model="button" ng-disabled="checked">Button</button> @@ -22054,7 +22649,7 @@ var htmlAnchorDirective = valueFn({ * attribute. See the {@link guide/interpolation interpolation guide} for more info. * * @example - <example> + <example name="ng-checked"> <file name="index.html"> <label>Check me to check both: <input type="checkbox" ng-model="master"></label><br/> <input id="checkSlave" type="checkbox" ng-checked="master" aria-label="Slave input"> @@ -22090,7 +22685,7 @@ var htmlAnchorDirective = valueFn({ * attribute. See the {@link guide/interpolation interpolation guide} for more info. * * @example - <example> + <example name="ng-readonly"> <file name="index.html"> <label>Check me to make text readonly: <input type="checkbox" ng-model="checked"></label><br/> <input type="text" ng-readonly="checked" value="I'm Angular" aria-label="Readonly field" /> @@ -22131,7 +22726,7 @@ var htmlAnchorDirective = valueFn({ * </div> * * @example - <example> + <example name="ng-selected"> <file name="index.html"> <label>Check me to select: <input type="checkbox" ng-model="selected"></label><br/> <select aria-label="ngSelected demo"> @@ -22172,7 +22767,7 @@ var htmlAnchorDirective = valueFn({ * recommended to use {@link ng.ngShow} and {@link ng.ngHide} instead. * * @example - <example> + <example name="ng-open"> <file name="index.html"> <label>Check me check multiple: <input type="checkbox" ng-model="open"></label><br/> <details id="details" ng-open="open"> @@ -22198,7 +22793,7 @@ var ngAttributeAliasDirectives = {}; // boolean attrs are evaluated forEach(BOOLEAN_ATTR, function(propName, attrName) { // binding to multiple is not supported - if (propName == "multiple") return; + if (propName === 'multiple') return; function defaultLinkFn(scope, element, attr) { scope.$watch(attr[normalized], function ngBooleanAttrWatchAction(value) { @@ -22235,10 +22830,10 @@ forEach(ALIASED_ATTR, function(htmlAttr, ngAttr) { 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) == "/") { + 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])); + attr.$set('ngPattern', new RegExp(match[1], match[2])); return; } } @@ -22278,10 +22873,11 @@ forEach(['src', 'srcset', 'href'], function(attrName) { attr.$set(name, value); - // on IE, if "ng:src" directive declaration is used and "src" attribute doesn't exist + // Support: IE 9-11 only + // On IE, if "ng:src" directive declaration is used and "src" attribute doesn't exist // then calling element.setAttribute('src', 'foo') doesn't do anything, so we need // to set the property as well to achieve the desired effect. - // we use attr[attrName] value since $set can sanitize the url. + // We use attr[attrName] value since $set can sanitize the url. if (msie && propName) element.prop(propName, attr[name]); }); } @@ -22289,7 +22885,7 @@ forEach(['src', 'srcset', 'href'], function(attrName) { }; }); -/* global -nullFormCtrl, -SUBMITTED_CLASS, addSetValidityMethod: true +/* global -nullFormCtrl, -PENDING_CLASS, -SUBMITTED_CLASS */ var nullFormCtrl = { $addControl: noop, @@ -22300,6 +22896,7 @@ var nullFormCtrl = { $setPristine: noop, $setSubmitted: noop }, +PENDING_CLASS = 'ng-pending', SUBMITTED_CLASS = 'ng-submitted'; function nullFormRenameControl(control, name) { @@ -22350,22 +22947,28 @@ function nullFormRenameControl(control, name) { */ //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 = []; +function FormController($element, $attrs, $scope, $animate, $interpolate) { + 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; + this.$error = {}; + this.$$success = {}; + this.$pending = undefined; + this.$name = $interpolate($attrs.name || $attrs.ngForm || '')($scope); + this.$dirty = false; + this.$pristine = true; + this.$valid = true; + this.$invalid = false; + this.$submitted = false; + this.$$parentForm = nullFormCtrl; + this.$$element = $element; + this.$$animate = $animate; + + setupValidity(this); +} + +FormController.prototype = { /** * @ngdoc method * @name form.FormController#$rollbackViewValue @@ -22377,11 +22980,11 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * 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) { + $rollbackViewValue: function() { + forEach(this.$$controls, function(control) { control.$rollbackViewValue(); }); - }; + }, /** * @ngdoc method @@ -22394,11 +22997,11 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * 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) { + $commitViewValue: function() { + forEach(this.$$controls, function(control) { control.$commitViewValue(); }); - }; + }, /** * @ngdoc method @@ -22421,29 +23024,29 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * 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) { + $addControl: function(control) { // Breaking change - before, inputs whose name was "hasOwnProperty" were quietly ignored // and not added to the scope. Now we throw an error. assertNotHasOwnProperty(control.$name, 'input'); - controls.push(control); + this.$$controls.push(control); if (control.$name) { - form[control.$name] = control; + this[control.$name] = control; } - control.$$parentForm = form; - }; + control.$$parentForm = this; + }, // Private API: rename a form control - form.$$renameControl = function(control, newName) { + $$renameControl: function(control, newName) { var oldName = control.$name; - if (form[oldName] === control) { - delete form[oldName]; + if (this[oldName] === control) { + delete this[oldName]; } - form[newName] = control; + this[newName] = control; control.$name = newName; - }; + }, /** * @ngdoc method @@ -22461,60 +23064,26 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * 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); + $removeControl: function(control) { + if (control.$name && this[control.$name] === control) { + delete this[control.$name]; + } + forEach(this.$pending, function(value, name) { + // eslint-disable-next-line no-invalid-this + this.$setValidity(name, null, control); + }, this); + forEach(this.$error, function(value, name) { + // eslint-disable-next-line no-invalid-this + this.$setValidity(name, null, control); + }, this); + forEach(this.$$success, function(value, name) { + // eslint-disable-next-line no-invalid-this + this.$setValidity(name, null, control); + }, this); + + arrayRemove(this.$$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 @@ -22526,13 +23095,13 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * 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(); - }; + $setDirty: function() { + this.$$animate.removeClass(this.$$element, PRISTINE_CLASS); + this.$$animate.addClass(this.$$element, DIRTY_CLASS); + this.$dirty = true; + this.$pristine = false; + this.$$parentForm.$setDirty(); + }, /** * @ngdoc method @@ -22541,22 +23110,24 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * @description * Sets the form to its pristine state. * - * This method can be called to remove the 'ng-dirty' class and set the form to its pristine - * state (ng-pristine class). This method will also propagate to all the controls contained - * in this form. + * This method sets the form's `$pristine` state to true, the `$dirty` state to false, removes + * the `ng-dirty` class and adds the `ng-pristine` class. Additionally, it sets the `$submitted` + * state to false. + * + * This method will also propagate to all the controls contained in this form. * * Setting a form back to a pristine state is often useful when we want to 'reuse' a form after * saving or resetting it. */ - form.$setPristine = function() { - $animate.setClass(element, PRISTINE_CLASS, DIRTY_CLASS + ' ' + SUBMITTED_CLASS); - form.$dirty = false; - form.$pristine = true; - form.$submitted = false; - forEach(controls, function(control) { + $setPristine: function() { + this.$$animate.setClass(this.$$element, PRISTINE_CLASS, DIRTY_CLASS + ' ' + SUBMITTED_CLASS); + this.$dirty = false; + this.$pristine = true; + this.$submitted = false; + forEach(this.$$controls, function(control) { control.$setPristine(); }); - }; + }, /** * @ngdoc method @@ -22571,11 +23142,11 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * 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) { + $setUntouched: function() { + forEach(this.$$controls, function(control) { control.$setUntouched(); }); - }; + }, /** * @ngdoc method @@ -22584,12 +23155,46 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * @description * Sets the form to its submitted state. */ - form.$setSubmitted = function() { - $animate.addClass(element, SUBMITTED_CLASS); - form.$submitted = true; - form.$$parentForm.$setSubmitted(); - }; -} + $setSubmitted: function() { + this.$$animate.addClass(this.$$element, SUBMITTED_CLASS); + this.$submitted = true; + this.$$parentForm.$setSubmitted(); + } +}; + +/** + * @ngdoc method + * @name form.FormController#$setValidity + * + * @description + * Sets the validity of a form control. + * + * This method will also propagate to parent forms. + */ +addSetValidityMethod({ + clazz: FormController, + set: function(object, property, controller) { + var list = object[property]; + if (!list) { + object[property] = [controller]; + } else { + var index = list.indexOf(controller); + if (index === -1) { + list.push(controller); + } + } + }, + unset: function(object, property, controller) { + var list = object[property]; + if (!list) { + return; + } + arrayRemove(list, controller); + if (list.length === 0) { + delete object[property]; + } + } +}); /** * @ngdoc directive @@ -22699,7 +23304,7 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * </pre> * * @example - <example deps="angular-animate.js" animations="true" fixBase="true" module="formExample"> + <example name="ng-form" deps="angular-animate.js" animations="true" fixBase="true" module="formExample"> <file name="index.html"> <script> angular.module('formExample', []) @@ -22786,13 +23391,13 @@ var formDirectiveFactory = function(isNgForm) { event.preventDefault(); }; - addEventListenerFn(formElement[0], 'submit', handleFormSubmission); + formElement[0].addEventListener('submit', handleFormSubmission); // unregister the preventDefault listener so that we don't not leak memory but in a // way that will achieve the prevention of the default action. formElement.on('$destroy', function() { $timeout(function() { - removeEventListenerFn(formElement[0], 'submit', handleFormSubmission); + formElement[0].removeEventListener('submit', handleFormSubmission); }, 0, false); }); } @@ -22837,13 +23442,117 @@ var formDirectiveFactory = function(isNgForm) { var formDirective = formDirectiveFactory(); var ngFormDirective = formDirectiveFactory(true); -/* global VALID_CLASS: false, + + +// helper methods +function setupValidity(instance) { + instance.$$classCache = {}; + instance.$$classCache[INVALID_CLASS] = !(instance.$$classCache[VALID_CLASS] = instance.$$element.hasClass(VALID_CLASS)); +} +function addSetValidityMethod(context) { + var clazz = context.clazz, + set = context.set, + unset = context.unset; + + clazz.prototype.$setValidity = function(validationErrorKey, state, controller) { + if (isUndefined(state)) { + createAndSet(this, '$pending', validationErrorKey, controller); + } else { + unsetAndCleanup(this, '$pending', validationErrorKey, controller); + } + if (!isBoolean(state)) { + unset(this.$error, validationErrorKey, controller); + unset(this.$$success, validationErrorKey, controller); + } else { + if (state) { + unset(this.$error, validationErrorKey, controller); + set(this.$$success, validationErrorKey, controller); + } else { + set(this.$error, validationErrorKey, controller); + unset(this.$$success, validationErrorKey, controller); + } + } + if (this.$pending) { + cachedToggleClass(this, PENDING_CLASS, true); + this.$valid = this.$invalid = undefined; + toggleValidationCss(this, '', null); + } else { + cachedToggleClass(this, PENDING_CLASS, false); + this.$valid = isObjectEmpty(this.$error); + this.$invalid = !this.$valid; + toggleValidationCss(this, '', this.$valid); + } + + // re-read the state as the set/unset methods could have + // combined state in this.$error[validationError] (used for forms), + // where setting/unsetting only increments/decrements the value, + // and does not replace it. + var combinedState; + if (this.$pending && this.$pending[validationErrorKey]) { + combinedState = undefined; + } else if (this.$error[validationErrorKey]) { + combinedState = false; + } else if (this.$$success[validationErrorKey]) { + combinedState = true; + } else { + combinedState = null; + } + + toggleValidationCss(this, validationErrorKey, combinedState); + this.$$parentForm.$setValidity(validationErrorKey, combinedState, this); + }; + + function createAndSet(ctrl, name, value, controller) { + if (!ctrl[name]) { + ctrl[name] = {}; + } + set(ctrl[name], value, controller); + } + + function unsetAndCleanup(ctrl, name, value, controller) { + if (ctrl[name]) { + unset(ctrl[name], value, controller); + } + if (isObjectEmpty(ctrl[name])) { + ctrl[name] = undefined; + } + } + + function cachedToggleClass(ctrl, className, switchValue) { + if (switchValue && !ctrl.$$classCache[className]) { + ctrl.$$animate.addClass(ctrl.$$element, className); + ctrl.$$classCache[className] = true; + } else if (!switchValue && ctrl.$$classCache[className]) { + ctrl.$$animate.removeClass(ctrl.$$element, className); + ctrl.$$classCache[className] = false; + } + } + + function toggleValidationCss(ctrl, validationErrorKey, isValid) { + validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : ''; + + cachedToggleClass(ctrl, VALID_CLASS + validationErrorKey, isValid === true); + cachedToggleClass(ctrl, INVALID_CLASS + validationErrorKey, isValid === false); + } +} + +function isObjectEmpty(obj) { + if (obj) { + for (var prop in obj) { + if (obj.hasOwnProperty(prop)) { + return false; + } + } + } + return true; +} + +/* global + VALID_CLASS: false, INVALID_CLASS: false, PRISTINE_CLASS: false, DIRTY_CLASS: false, - UNTOUCHED_CLASS: false, - TOUCHED_CLASS: false, - ngModelMinErr: false, + ngModelMinErr: false */ // Regex code was initially obtained from SO prior to modification: https://stackoverflow.com/questions/3143070/javascript-regex-iso-datetime#answer-3143231 @@ -22859,12 +23568,11 @@ var ISO_DATE_REGEXP = /^\d{4,}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+(?:[+- // 7. Path // 8. Query // 9. Fragment -// 1111111111111111 222 333333 44444 555555555555555555555555 666 77777777 8888888 999 -var URL_REGEXP = /^[a-z][a-z\d.+-]*:\/*(?:[^:@]+(?::[^@]+)?@)?(?:[^\s:/?#]+|\[[a-f\d:]+\])(?::\d+)?(?:\/[^?#]*)?(?:\?[^#]*)?(?:#.*)?$/i; -/* jshint maxlen:220 */ -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])?)*$/; -/* jshint maxlen:200 */ -var NUMBER_REGEXP = /^\s*(\-|\+)?(\d+|(\d*(\.\d*)))([eE][+-]?\d+)?\s*$/; +// 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)$/; @@ -23044,7 +23752,6 @@ var inputType = { <file name="protractor.js" type="protractor"> var value = element(by.binding('example.value | date: "yyyy-MM-dd"')); var valid = element(by.binding('myForm.input.$valid')); - var input = element(by.model('example.value')); // currently protractor/webdriver does not support // sending keys to all known HTML5 input controls @@ -23147,7 +23854,6 @@ var inputType = { <file name="protractor.js" type="protractor"> var value = element(by.binding('example.value | date: "yyyy-MM-ddTHH:mm:ss"')); var valid = element(by.binding('myForm.input.$valid')); - var input = element(by.model('example.value')); // currently protractor/webdriver does not support // sending keys to all known HTML5 input controls @@ -23251,7 +23957,6 @@ var inputType = { <file name="protractor.js" type="protractor"> var value = element(by.binding('example.value | date: "HH:mm:ss"')); var valid = element(by.binding('myForm.input.$valid')); - var input = element(by.model('example.value')); // currently protractor/webdriver does not support // sending keys to all known HTML5 input controls @@ -23356,7 +24061,6 @@ var inputType = { <file name="protractor.js" type="protractor"> var value = element(by.binding('example.value | date: "yyyy-Www"')); var valid = element(by.binding('myForm.input.$valid')); - var input = element(by.model('example.value')); // currently protractor/webdriver does not support // sending keys to all known HTML5 input controls @@ -23460,7 +24164,6 @@ var inputType = { <file name="protractor.js" type="protractor"> var value = element(by.binding('example.value | date: "yyyy-MM"')); var valid = element(by.binding('myForm.input.$valid')); - var input = element(by.model('example.value')); // currently protractor/webdriver does not support // sending keys to all known HTML5 input controls @@ -23523,7 +24226,17 @@ var inputType = { * @param {string} ngModel Assignable angular expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. + * Can be interpolated. * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. + * Can be interpolated. + * @param {string=} ngMin Like `min`, sets the `min` validation error key if the value entered is less than `ngMin`, + * but does not trigger HTML5 native validation. Takes an expression. + * @param {string=} ngMax Like `max`, sets the `max` validation error key if the value entered is greater than `ngMax`, + * but does not trigger HTML5 native validation. Takes an expression. + * @param {string=} step Sets the `step` validation error key if the value entered does not fit the `step` constraint. + * Can be interpolated. + * @param {string=} ngStep Like `step`, sets the `step` validation error key if the value entered does not fit the `ngStep` constraint, + * but does not trigger HTML5 native validation. Takes an expression. * @param {string=} required Sets `required` validation error key if the value is not entered. * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of @@ -23855,19 +24568,140 @@ var inputType = { </file> <file name="protractor.js" type="protractor"> 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'); - element.all(by.model('color.name')).get(0).click(); - + inputs.get(0).click(); expect(color.getText()).toContain('red'); + + inputs.get(1).click(); + expect(color.getText()).toContain('green'); }); </file> </example> */ 'radio': radioInputType, + /** + * @ngdoc input + * @name input[range] + * + * @description + * Native range input with validation and transformation. + * + * The model for the range input must always be a `Number`. + * + * IE9 and other browsers that do not support the `range` type fall back + * to a text input without any default values for `min`, `max` and `step`. Model binding, + * validation and number parsing are nevertheless supported. + * + * Browsers that support range (latest Chrome, Safari, Firefox, Edge) treat `input[range]` + * in a way that never allows the input to hold an invalid value. That means: + * - any non-numerical value is set to `(max + min) / 2`. + * - any numerical value that is less than the current min val, or greater than the current max val + * is set to the min / max val respectively. + * - additionally, the current `step` is respected, so the nearest value that satisfies a step + * is used. + * + * See the [HTML Spec on input[type=range]](https://www.w3.org/TR/html5/forms.html#range-state-(type=range)) + * for more info. + * + * This has the following consequences for Angular: + * + * Since the element value should always reflect the current model value, a range input + * will set the bound ngModel expression to the value that the browser has set for the + * input element. For example, in the following input `<input type="range" ng-model="model.value">`, + * if the application sets `model.value = null`, the browser will set the input to `'50'`. + * Angular will then set the model to `50`, to prevent input and model value being out of sync. + * + * That means the model for range will immediately be set to `50` after `ngModel` has been + * initialized. It also means a range input can never have the required error. + * + * This does not only affect changes to the model value, but also to the values of the `min`, + * `max`, and `step` attributes. When these change in a way that will cause the browser to modify + * the input value, Angular will also update the model value. + * + * Automatic value adjustment also means that a range input element can never have the `required`, + * `min`, or `max` errors. + * + * However, `step` is currently only fully implemented by Firefox. Other browsers have problems + * when the step value changes dynamically - they do not adjust the element value correctly, but + * instead may set the `stepMismatch` error. If that's the case, the Angular will set the `step` + * error on the input, and set the model to `undefined`. + * + * Note that `input[range]` is not compatible with`ngMax`, `ngMin`, and `ngStep`, because they do + * not set the `min` and `max` attributes, which means that the browser won't automatically adjust + * the input value based on their values, and will always assume min = 0, max = 100, and step = 1. + * + * @param {string} ngModel Assignable angular expression to data-bind to. + * @param {string=} name Property name of the form under which the control is published. + * @param {string=} min Sets the `min` validation to ensure that the value entered is greater + * than `min`. Can be interpolated. + * @param {string=} max Sets the `max` validation to ensure that the value entered is less than `max`. + * Can be interpolated. + * @param {string=} step Sets the `step` validation to ensure that the value entered matches the `step` + * Can be interpolated. + * @param {string=} ngChange Angular expression to be executed when the ngModel value changes due + * to user interaction with the input element. + * @param {expression=} ngChecked If the expression is truthy, then the `checked` attribute will be set on the + * element. **Note** : `ngChecked` should not be used alongside `ngModel`. + * Checkout {@link ng.directive:ngChecked ngChecked} for usage. + * + * @example + <example name="range-input-directive" module="rangeExample"> + <file name="index.html"> + <script> + angular.module('rangeExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.value = 75; + $scope.min = 10; + $scope.max = 90; + }]); + </script> + <form name="myForm" ng-controller="ExampleController"> + + Model as range: <input type="range" name="range" ng-model="value" min="{{min}}" max="{{max}}"> + <hr> + Model as number: <input type="number" ng-model="value"><br> + Min: <input type="number" ng-model="min"><br> + Max: <input type="number" ng-model="max"><br> + value = <code>{{value}}</code><br/> + myForm.range.$valid = <code>{{myForm.range.$valid}}</code><br/> + myForm.range.$error = <code>{{myForm.range.$error}}</code> + </form> + </file> + </example> + + * ## Range Input with ngMin & ngMax attributes + + * @example + <example name="range-input-directive-ng" module="rangeExample"> + <file name="index.html"> + <script> + angular.module('rangeExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.value = 75; + $scope.min = 10; + $scope.max = 90; + }]); + </script> + <form name="myForm" ng-controller="ExampleController"> + Model as range: <input type="range" name="range" ng-model="value" ng-min="min" ng-max="max"> + <hr> + Model as number: <input type="number" ng-model="value"><br> + Min: <input type="number" ng-model="min"><br> + Max: <input type="number" ng-model="max"><br> + value = <code>{{value}}</code><br/> + myForm.range.$valid = <code>{{myForm.range.$valid}}</code><br/> + myForm.range.$error = <code>{{myForm.range.$error}}</code> + </form> + </file> + </example> + + */ + 'range': rangeInputType, /** * @ngdoc input @@ -23947,7 +24781,7 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) { var type = lowercase(element[0].type); - // In composition mode, users are still inputing intermediate text buffer, + // 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) { @@ -24005,7 +24839,7 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) { } }; - element.on('keydown', function(event) { + element.on('keydown', /** @this */ function(event) { var key = event.keyCode; // ignore @@ -24030,7 +24864,7 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) { // 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, function(ev) { + element.on(PARTIAL_VALIDATION_EVENTS, /** @this */ function(ev) { if (!timeout) { var validity = this[VALIDITY_STATE_PROPERTY]; var origBadInput = validity.badInput; @@ -24098,7 +24932,7 @@ function createDateParser(regexp, mapping) { // 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) == '"') { + if (iso.charAt(0) === '"' && iso.charAt(iso.length - 1) === '"') { iso = iso.substring(1, iso.length - 1); } if (ISO_DATE_REGEXP.test(iso)) { @@ -24140,7 +24974,7 @@ 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 timezone = ctrl && ctrl.$options.getOption('timezone'); var previousDate; ctrl.$$parserName = type; @@ -24219,10 +25053,7 @@ function badInputChecker(scope, element, attr, ctrl) { } } -function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { - badInputChecker(scope, element, attr, ctrl); - baseInputType(scope, element, attr, ctrl, $sniffer, $browser); - +function numberFormatterParser(ctrl) { ctrl.$$parserName = 'number'; ctrl.$parsers.push(function(value) { if (ctrl.$isEmpty(value)) return null; @@ -24239,40 +25070,255 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { } 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); + + var isNonIntegerValue = !isNumberInteger(value); + var isNonIntegerStepBase = !isNumberInteger(stepBase); + var isNonIntegerStep = !isNumberInteger(step); + + // Due to limitations in Floating Point Arithmetic (e.g. `0.3 - 0.2 !== 0.1` or + // `0.5 % 0.1 !== 0`), we need to convert all numbers to integers. + if (isNonIntegerValue || isNonIntegerStepBase || isNonIntegerStep) { + var valueDecimals = isNonIntegerValue ? countDecimals(value) : 0; + var stepBaseDecimals = isNonIntegerStepBase ? countDecimals(stepBase) : 0; + var stepDecimals = isNonIntegerStep ? countDecimals(step) : 0; + + var decimalCount = Math.max(valueDecimals, stepBaseDecimals, stepDecimals); + var multiplier = Math.pow(10, decimalCount); + + value = value * multiplier; + stepBase = stepBase * multiplier; + step = step * multiplier; + + if (isNonIntegerValue) value = Math.round(value); + if (isNonIntegerStepBase) stepBase = Math.round(stepBase); + if (isNonIntegerStep) step = Math.round(step); + } + + return (value - stepBase) % step === 0; +} + +function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { + badInputChecker(scope, element, attr, ctrl); + numberFormatterParser(ctrl); + baseInputType(scope, element, attr, ctrl, $sniffer, $browser); + + var minVal; + var maxVal; if (isDefined(attr.min) || attr.ngMin) { - var minVal; ctrl.$validators.min = function(value) { return ctrl.$isEmpty(value) || isUndefined(minVal) || value >= minVal; }; attr.$observe('min', function(val) { - if (isDefined(val) && !isNumber(val)) { - val = parseFloat(val); - } - minVal = isNumber(val) && !isNaN(val) ? val : undefined; + minVal = parseNumberAttrVal(val); // TODO(matsko): implement validateLater to reduce number of validations ctrl.$validate(); }); } if (isDefined(attr.max) || attr.ngMax) { - var maxVal; ctrl.$validators.max = function(value) { return ctrl.$isEmpty(value) || isUndefined(maxVal) || value <= maxVal; }; attr.$observe('max', function(val) { - if (isDefined(val) && !isNumber(val)) { - val = parseFloat(val); - } - maxVal = isNumber(val) && !isNaN(val) ? val : undefined; + maxVal = parseNumberAttrVal(val); + // TODO(matsko): implement validateLater to reduce number of validations + ctrl.$validate(); + }); + } + + if (isDefined(attr.step) || attr.ngStep) { + var stepVal; + ctrl.$validators.step = function(modelValue, viewValue) { + return ctrl.$isEmpty(viewValue) || isUndefined(stepVal) || + isValidForStep(viewValue, minVal || 0, stepVal); + }; + + attr.$observe('step', function(val) { + stepVal = parseNumberAttrVal(val); // TODO(matsko): implement validateLater to reduce number of validations ctrl.$validate(); }); } } +function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) { + badInputChecker(scope, element, attr, ctrl); + numberFormatterParser(ctrl); + baseInputType(scope, element, attr, ctrl, $sniffer, $browser); + + var supportsRange = ctrl.$$hasNativeValidators && element[0].type === 'range', + minVal = supportsRange ? 0 : undefined, + maxVal = supportsRange ? 100 : undefined, + stepVal = supportsRange ? 1 : undefined, + validity = element[0].validity, + hasMinAttr = isDefined(attr.min), + hasMaxAttr = isDefined(attr.max), + hasStepAttr = isDefined(attr.step); + + var originalRender = ctrl.$render; + + ctrl.$render = supportsRange && isDefined(validity.rangeUnderflow) && isDefined(validity.rangeOverflow) ? + //Browsers that implement range will set these values automatically, but reading the adjusted values after + //$render would cause the min / max validators to be applied with the wrong value + function rangeRender() { + originalRender(); + ctrl.$setViewValue(element.val()); + } : + originalRender; + + if (hasMinAttr) { + ctrl.$validators.min = supportsRange ? + // Since all browsers set the input to a valid value, we don't need to check validity + function noopMinValidator() { return true; } : + // non-support browsers validate the min val + function minValidator(modelValue, viewValue) { + return ctrl.$isEmpty(viewValue) || isUndefined(minVal) || viewValue >= minVal; + }; + + setInitialValueAndObserver('min', minChange); + } + + if (hasMaxAttr) { + ctrl.$validators.max = supportsRange ? + // Since all browsers set the input to a valid value, we don't need to check validity + function noopMaxValidator() { return true; } : + // non-support browsers validate the max val + function maxValidator(modelValue, viewValue) { + return ctrl.$isEmpty(viewValue) || isUndefined(maxVal) || viewValue <= maxVal; + }; + + setInitialValueAndObserver('max', maxChange); + } + + if (hasStepAttr) { + ctrl.$validators.step = supportsRange ? + function nativeStepValidator() { + // Currently, only FF implements the spec on step change correctly (i.e. adjusting the + // input element value to a valid value). It's possible that other browsers set the stepMismatch + // validity error instead, so we can at least report an error in that case. + return !validity.stepMismatch; + } : + // ngStep doesn't set the setp attr, so the browser doesn't adjust the input value as setting step would + function stepValidator(modelValue, viewValue) { + return ctrl.$isEmpty(viewValue) || isUndefined(stepVal) || + isValidForStep(viewValue, minVal || 0, stepVal); + }; + + setInitialValueAndObserver('step', stepChange); + } + + function setInitialValueAndObserver(htmlAttrName, changeFn) { + // interpolated attributes set the attribute value only after a digest, but we need the + // attribute value when the input is first rendered, so that the browser can adjust the + // input value based on the min/max value + element.attr(htmlAttrName, attr[htmlAttrName]); + attr.$observe(htmlAttrName, changeFn); + } + + function minChange(val) { + minVal = parseNumberAttrVal(val); + // ignore changes before model is initialized + if (isNumberNaN(ctrl.$modelValue)) { + return; + } + + 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! @@ -24300,14 +25346,20 @@ function emailInputType(scope, element, attr, ctrl, $sniffer, $browser) { } function radioInputType(scope, element, attr, ctrl) { + var doTrim = !attr.ngTrim || trim(attr.ngTrim) !== 'false'; // make the name unique, if not defined if (isUndefined(attr.name)) { element.attr('name', nextUid()); } var listener = function(ev) { + var value; if (element[0].checked) { - ctrl.$setViewValue(attr.value, ev && ev.type); + value = attr.value; + if (doTrim) { + value = trim(value); + } + ctrl.$setViewValue(value, ev && ev.type); } }; @@ -24315,7 +25367,10 @@ function radioInputType(scope, element, attr, ctrl) { ctrl.$render = function() { var value = attr.value; - element[0].checked = (value == ctrl.$viewValue); + if (doTrim) { + value = trim(value); + } + element[0].checked = (value === ctrl.$viewValue); }; attr.$observe('value', ctrl.$render); @@ -24398,6 +25453,20 @@ function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filt * @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 `<textarea>`, Internet Explorer will temporarily + * insert the placeholder value as the textarea's content. If the placeholder value contains + * interpolation (`{{ ... }}`), an error will be logged in the console when Angular tries to update + * the value of the by-then-removed text node. This doesn't affect the functionality of the + * textarea, but can be undesirable. + * + * You can work around this Internet Explorer issue by using `ng-attr-placeholder` instead of + * `placeholder` on textareas, whenever you need interpolation in the placeholder value. You can + * find more details on `ngAttr` in the + * [Interpolation](guide/interpolation#-ngattr-for-binding-to-arbitrary-attributes) section of the + * Developer Guide. */ @@ -24562,21 +25631,19 @@ var CONSTANT_VALUE_REGEXP = /^(true|false|\d+)$/; * @name ngValue * * @description - * Binds the given expression to the value of `<option>` or {@link input[radio] `input[radio]`}, - * so that when the element is selected, the {@link ngModel `ngModel`} of that element is set to - * the bound value. + * Binds the given expression to the value of the element. * - * `ngValue` is useful when dynamically generating lists of radio buttons using - * {@link ngRepeat `ngRepeat`}, as shown below. + * It is mainly used on {@link input[radio] `input[radio]`} and option elements, + * so that when the element is selected, the {@link ngModel `ngModel`} of that element (or its + * {@link select `select`} parent element) is set to the bound value. It is especially useful + * for dynamically generated lists using {@link ngRepeat `ngRepeat`}, as shown below. * - * Likewise, `ngValue` can be used to generate `<option>` elements for - * the {@link select `select`} element. In that case however, only strings are supported - * for the `value `attribute, so the resulting `ngModel` will always be a string. - * Support for `select` models with non-string values is available via `ngOptions`. + * It can also be used to achieve one-way binding of a given expression to an input element + * such as an `input[text]` or a `textarea`, when that element does not use ngModel. * * @element input * @param {string=} ngValue angular expression, whose value will be bound to the `value` attribute - * of the `input` element + * and `value` property of the element. * * @example <example name="ngValue-directive" module="valueExample"> @@ -24615,18 +25682,33 @@ var CONSTANT_VALUE_REGEXP = /^(true|false|\d+)$/; </example> */ var ngValueDirective = function() { + /** + * inputs use the value attribute as their default value if the value property is not set. + * Once the value property has been set (by adding input), it will not react to changes to + * the value attribute anymore. Setting both attribute and property fixes this behavior, and + * makes it possible to use ngValue as a sort of one-way bind. + */ + function updateElementValue(element, attr, value) { + // Support: IE9 only + // In IE9 values are converted to string (e.g. `input.value = null` results in `input.value === 'null'`). + var propValue = isDefined(value) ? value : (msie === 9) ? '' : null; + element.prop('value', propValue); + attr.$set('value', value); + } + return { restrict: 'A', priority: 100, compile: function(tpl, tplAttr) { if (CONSTANT_VALUE_REGEXP.test(tplAttr.ngValue)) { return function ngValueConstantLink(scope, elm, attr) { - attr.$set('value', scope.$eval(attr.ngValue)); + var value = scope.$eval(attr.ngValue); + updateElementValue(elm, attr, value); }; } else { return function ngValueLink(scope, elm, attr) { scope.$watch(attr.ngValue, function valueWatchAction(value) { - attr.$set('value', value); + updateElementValue(elm, attr, value); }); }; } @@ -24660,7 +25742,7 @@ var ngValueDirective = function() { * * @example * Enter a name in the Live Preview text box; the greeting below the text box changes instantly. - <example module="bindExample"> + <example module="bindExample" name="ng-bind"> <file name="index.html"> <script> angular.module('bindExample', []) @@ -24694,7 +25776,7 @@ var ngBindDirective = ['$compile', function($compile) { $compile.$$addBindingInfo(element, attr.ngBind); element = element[0]; scope.$watch(attr.ngBind, function ngBindWatchAction(value) { - element.textContent = isUndefined(value) ? '' : value; + element.textContent = stringify(value); }); }; } @@ -24720,7 +25802,7 @@ var ngBindDirective = ['$compile', function($compile) { * * @example * Try it here: enter text in text box and watch the greeting change. - <example module="bindExample"> + <example module="bindExample" name="ng-bind-template"> <file name="index.html"> <script> angular.module('bindExample', []) @@ -24793,7 +25875,7 @@ var ngBindTemplateDirective = ['$interpolate', '$compile', function($interpolate * * @example - <example module="bindHtmlExample" deps="angular-sanitize.js"> + <example module="bindHtmlExample" deps="angular-sanitize.js" name="ng-bind-html"> <file name="index.html"> <div ng-controller="ExampleController"> <p ng-bind-html="myHTML"></p> @@ -24918,50 +26000,72 @@ var ngChangeDirective = valueFn({ } }); +/* exported + ngClassDirective, + ngClassEvenDirective, + ngClassOddDirective +*/ + function classDirective(name, selector) { name = 'ngClass' + name; - return ['$animate', function($animate) { + var indexWatchExpression; + + return ['$parse', function($parse) { return { restrict: 'AC', link: function(scope, element, attr) { - var oldVal; + var classCounts = element.data('$classCounts'); + var oldModulo = true; + var oldClassString; - scope.$watch(attr[name], ngClassWatchAction, true); + if (!classCounts) { + // Use createMap() to prevent class assumptions involving property + // names in Object.prototype + classCounts = createMap(); + element.data('$classCounts', classCounts); + } - attr.$observe('class', function(value) { - ngClassWatchAction(scope.$eval(attr[name])); - }); + if (name !== 'ngClass') { + if (!indexWatchExpression) { + indexWatchExpression = $parse('$index', function moduloTwo($index) { + // eslint-disable-next-line no-bitwise + return $index & 1; + }); + } + scope.$watch(indexWatchExpression, ngClassIndexWatchAction); + } - if (name !== 'ngClass') { - scope.$watch('$index', function($index, old$index) { - // jshint bitwise: false - var mod = $index & 1; - if (mod !== (old$index & 1)) { - var classes = arrayClasses(scope.$eval(attr[name])); - mod === selector ? - addClasses(classes) : - removeClasses(classes); - } - }); + scope.$watch($parse(attr[name], toClassString), ngClassWatchAction); + + function addClasses(classString) { + classString = digestClassCounts(split(classString), 1); + attr.$addClass(classString); } - function addClasses(classes) { - var newClasses = digestClassCounts(classes, 1); - attr.$addClass(newClasses); + function removeClasses(classString) { + classString = digestClassCounts(split(classString), -1); + attr.$removeClass(classString); } - function removeClasses(classes) { - var newClasses = digestClassCounts(classes, -1); - attr.$removeClass(newClasses); + function updateClasses(oldClassString, newClassString) { + var oldClassArray = split(oldClassString); + var newClassArray = split(newClassString); + + var toRemoveArray = arrayDifference(oldClassArray, newClassArray); + var toAddArray = arrayDifference(newClassArray, oldClassArray); + + var toRemoveString = digestClassCounts(toRemoveArray, -1); + var toAddString = digestClassCounts(toAddArray, 1); + + attr.$addClass(toAddString); + attr.$removeClass(toRemoveString); } - function digestClassCounts(classes, count) { - // Use createMap() to prevent class assumptions involving property - // names in Object.prototype - var classCounts = element.data('$classCounts') || createMap(); + function digestClassCounts(classArray, count) { var classesToUpdate = []; - forEach(classes, function(className) { + + forEach(classArray, function(className) { if (count > 0 || classCounts[className]) { classCounts[className] = (classCounts[className] || 0) + count; if (classCounts[className] === +(count > 0)) { @@ -24969,78 +26073,76 @@ function classDirective(name, selector) { } } }); - element.data('$classCounts', classCounts); + return classesToUpdate.join(' '); } - function updateClasses(oldClasses, newClasses) { - var toAdd = arrayDifference(newClasses, oldClasses); - var toRemove = arrayDifference(oldClasses, newClasses); - toAdd = digestClassCounts(toAdd, 1); - toRemove = digestClassCounts(toRemove, -1); - if (toAdd && toAdd.length) { - $animate.addClass(element, toAdd); - } - if (toRemove && toRemove.length) { - $animate.removeClass(element, toRemove); + function ngClassIndexWatchAction(newModulo) { + // This watch-action should run before the `ngClassWatchAction()`, thus it + // adds/removes `oldClassString`. If the `ngClass` expression has changed as well, the + // `ngClassWatchAction()` will update the classes. + if (newModulo === selector) { + addClasses(oldClassString); + } else { + removeClasses(oldClassString); } + + oldModulo = newModulo; } - function ngClassWatchAction(newVal) { - // jshint bitwise: false - if (selector === true || (scope.$index & 1) === selector) { - // jshint bitwise: true - var newClasses = arrayClasses(newVal || []); - if (!oldVal) { - addClasses(newClasses); - } else if (!equals(newVal,oldVal)) { - var oldClasses = arrayClasses(oldVal); - updateClasses(oldClasses, newClasses); - } + function ngClassWatchAction(newClassString) { + // When using a one-time binding the newClassString will return + // the pre-interceptor value until the one-time is complete + if (!isString(newClassString)) { + newClassString = toClassString(newClassString); } - if (isArray(newVal)) { - oldVal = newVal.map(function(v) { return shallowCopy(v); }); - } else { - oldVal = shallowCopy(newVal); + + if (oldModulo === selector) { + updateClasses(oldClassString, newClassString); } + + oldClassString = newClassString; } } }; + }]; - function arrayDifference(tokens1, tokens2) { - var values = []; + // Helpers + function arrayDifference(tokens1, tokens2) { + if (!tokens1 || !tokens1.length) return []; + if (!tokens2 || !tokens2.length) return tokens1; - outer: - for (var i = 0; i < tokens1.length; i++) { - var token = tokens1[i]; - for (var j = 0; j < tokens2.length; j++) { - if (token == tokens2[j]) continue outer; - } - values.push(token); + var values = []; + + outer: + for (var i = 0; i < tokens1.length; i++) { + var token = tokens1[i]; + for (var j = 0; j < tokens2.length; j++) { + if (token === tokens2[j]) continue outer; } - return values; + values.push(token); } - function arrayClasses(classVal) { - var classes = []; - if (isArray(classVal)) { - forEach(classVal, function(v) { - classes = classes.concat(arrayClasses(v)); - }); - return classes; - } else if (isString(classVal)) { - return classVal.split(' '); - } else if (isObject(classVal)) { - forEach(classVal, function(v, k) { - if (v) { - classes = classes.concat(k.split(' ')); - } - }); - return classes; - } - return classVal; + return values; + } + + function split(classString) { + return classString && classString.split(' '); + } + + function toClassString(classValue) { + var classString = classValue; + + if (isArray(classValue)) { + classString = classValue.map(toClassString).join(' '); + } else if (isObject(classValue)) { + classString = Object.keys(classValue). + filter(function(key) { return classValue[key]; }). + join(' '); } - }]; + + return classString; + } } /** @@ -25090,7 +26192,7 @@ function classDirective(name, selector) { * element. * * @example Example that demonstrates basic bindings via ngClass directive. - <example> + <example name="ng-class"> <file name="index.html"> <p ng-class="{strike: deleted, bold: important, 'has-error': error}">Map Syntax Example</p> <label> @@ -25183,7 +26285,7 @@ function classDirective(name, selector) { The example below demonstrates how to perform animations using ngClass. - <example module="ngAnimate" deps="angular-animate.js" animations="true"> + <example module="ngAnimate" deps="angular-animate.js" animations="true" name="ng-class"> <file name="index.html"> <input id="setbtn" type="button" value="set" ng-click="myVar='my-class'"> <input id="clearbtn" type="button" value="clear" ng-click="myVar=''"> @@ -25246,7 +26348,7 @@ var ngClassDirective = classDirective('', true); * of the evaluation can be a string representing space delimited class names or an array. * * @example - <example> + <example name="ng-class-odd"> <file name="index.html"> <ol ng-init="names=['John', 'Mary', 'Cate', 'Suz']"> <li ng-repeat="name in names"> @@ -25294,7 +26396,7 @@ var ngClassOddDirective = classDirective('Odd', 0); * result of the evaluation can be a string representing space delimited class names or an array. * * @example - <example> + <example name="ng-class-even"> <file name="index.html"> <ol ng-init="names=['John', 'Mary', 'Cate', 'Suz']"> <li ng-repeat="name in names"> @@ -25360,7 +26462,7 @@ var ngClassEvenDirective = classDirective('Even', 1); * @element ANY * * @example - <example> + <example name="ng-cloak"> <file name="index.html"> <div id="template1" ng-cloak>{{ 'hello' }}</div> <div id="template2" class="ng-cloak">{{ 'world' }}</div> @@ -25416,7 +26518,7 @@ var ngCloakDirective = ngDirective({ * * If the current `$controllerProvider` is configured to use globals (via * {@link ng.$controllerProvider#allowGlobals `$controllerProvider.allowGlobals()` }), this may - * also be the name of a globally accessible constructor function (not recommended). + * also be the name of a globally accessible constructor function (deprecated, not recommended). * * @example * Here is a simple form for editing user contact information. Adding, removing, clearing, and @@ -25469,10 +26571,11 @@ var ngCloakDirective = ngDirective({ * .controller('SettingsController1', SettingsController1); * * function SettingsController1() { - * this.name = "John Smith"; + * this.name = 'John Smith'; * this.contacts = [ * {type: 'phone', value: '408 555 1212'}, - * {type: 'email', value: 'john.smith@example.org'} ]; + * {type: 'email', value: 'john.smith@example.org'} + * ]; * } * * SettingsController1.prototype.greet = function() { @@ -25552,10 +26655,11 @@ var ngCloakDirective = ngDirective({ * .controller('SettingsController2', ['$scope', SettingsController2]); * * function SettingsController2($scope) { - * $scope.name = "John Smith"; + * $scope.name = 'John Smith'; * $scope.contacts = [ * {type:'phone', value:'408 555 1212'}, - * {type:'email', value:'john.smith@example.org'} ]; + * {type:'email', value:'john.smith@example.org'} + * ]; * * $scope.greet = function() { * alert($scope.name); @@ -25622,31 +26726,38 @@ var ngControllerDirective = [function() { * @ngdoc directive * @name ngCsp * - * @element html + * @restrict A + * @element ANY * @description * - * Angular has some features that can break certain + * Angular has some features that can conflict with certain restrictions that are applied when using * [CSP (Content Security Policy)](https://developer.mozilla.org/en/Security/CSP) rules. * - * If you intend to implement these rules then you must tell Angular not to use these features. + * If you intend to implement CSP with these rules then you must tell Angular not to use these + * features. * * This is necessary when developing things like Google Chrome Extensions or Universal Windows Apps. * * - * The following rules affect Angular: + * The following default rules in CSP affect Angular: * - * * `unsafe-eval`: this rule forbids apps to use `eval` or `Function(string)` generated functions - * (among other things). Angular makes use of this in the {@link $parse} service to provide a 30% - * increase in the speed of evaluating Angular expressions. + * * The use of `eval()`, `Function(string)` and similar functions to dynamically create and execute + * code from strings is forbidden. Angular makes use of this in the {@link $parse} service to + * provide a 30% increase in the speed of evaluating Angular expressions. (This CSP rule can be + * disabled with the CSP keyword `unsafe-eval`, but it is generally not recommended as it would + * weaken the protections offered by CSP.) * - * * `unsafe-inline`: this rule forbids apps from inject custom styles into the document. Angular - * makes use of this to include some CSS rules (e.g. {@link ngCloak} and {@link ngHide}). - * To make these directives work when a CSP rule is blocking inline styles, you must link to the - * `angular-csp.css` in your HTML manually. + * * The use of inline resources, such as inline `<script>` and `<style>` elements, are forbidden. + * This prevents apps from injecting custom styles directly into the document. Angular makes use of + * this to include some CSS rules (e.g. {@link ngCloak} and {@link ngHide}). To make these + * directives work when a CSP rule is blocking inline styles, you must link to the `angular-csp.css` + * in your HTML manually. (This CSP rule can be disabled with the CSP keyword `unsafe-inline`, but + * it is generally not recommended as it would weaken the protections offered by CSP.) * - * If you do not provide `ngCsp` then Angular tries to autodetect if CSP is blocking unsafe-eval - * and automatically deactivates this feature in the {@link $parse} service. This autodetection, - * however, triggers a CSP error to be logged in the console: + * If you do not provide `ngCsp` then Angular tries to autodetect if CSP is blocking dynamic code + * creation from strings (e.g., `unsafe-eval` not specified in CSP header) and automatically + * deactivates this feature in the {@link $parse} service. This autodetection, however, triggers a + * CSP error to be logged in the console: * * ``` * Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of @@ -25671,15 +26782,15 @@ var ngControllerDirective = [function() { * * * * No declaration means that Angular will assume that you can do inline styles, but it will do - * a runtime check for unsafe-eval. E.g. `<body>`. This is backwardly compatible with previous versions - * of Angular. + * a runtime check for unsafe-eval. E.g. `<body>`. This is backwardly compatible with previous + * versions of Angular. * * * A simple `ng-csp` (or `data-ng-csp`) attribute will tell Angular to deactivate both inline - * styles and unsafe eval. E.g. `<body ng-csp>`. This is backwardly compatible with previous versions - * of Angular. + * styles and unsafe eval. E.g. `<body ng-csp>`. This is backwardly compatible with previous + * versions of Angular. * - * * Specifying only `no-unsafe-eval` tells Angular that we must not use eval, but that we can inject - * inline styles. E.g. `<body ng-csp="no-unsafe-eval">`. + * * Specifying only `no-unsafe-eval` tells Angular that we must not use eval, but that we can + * inject inline styles. E.g. `<body ng-csp="no-unsafe-eval">`. * * * Specifying only `no-inline-style` tells Angular that we must not inject styles, but that we can * run eval - no automatic check for unsafe eval will occur. E.g. `<body ng-csp="no-inline-style">` @@ -25698,8 +26809,7 @@ var ngControllerDirective = [function() { </html> ``` * @example - // Note: the suffix `.csp` in the example name triggers - // csp mode in our http server! + <!-- Note: the `.csp` suffix in the example name triggers CSP mode in our http server! --> <example name="example.csp" module="cspExample" ng-csp="true"> <file name="index.html"> <div ng-controller="MainController as ctrl"> @@ -25720,15 +26830,14 @@ var ngControllerDirective = [function() { </file> <file name="script.js"> angular.module('cspExample', []) - .controller('MainController', function() { + .controller('MainController', function MainController() { this.counter = 0; this.inc = function() { this.counter++; }; this.evil = function() { - // jshint evil:true try { - eval('1+2'); + eval('1+2'); // eslint-disable-line no-eval } catch (e) { this.evilError = e.message; } @@ -25780,7 +26889,7 @@ var ngControllerDirective = [function() { beforeEach(function() { util = require('util'); - webdriver = require('protractor/node_modules/selenium-webdriver'); + webdriver = require('selenium-webdriver'); }); // For now, we only test on Chrome, @@ -25817,9 +26926,9 @@ var ngControllerDirective = [function() { </example> */ -// ngCsp is not implemented as a proper directive any more, because we need it be processed while we -// bootstrap the system (before $parse is instantiated), for this reason we just have -// the csp() fn that looks for the `ng-csp` attribute anywhere in the current doc +// `ngCsp` is not implemented as a proper directive any more, because we need it be processed while +// we bootstrap the app (before `$parse` is instantiated). For this reason, we just have the `csp()` +// fn that looks for the `ng-csp` attribute anywhere in the current doc. /** * @ngdoc directive @@ -25835,7 +26944,7 @@ var ngControllerDirective = [function() { * click. ({@link guide/expression#-event- Event object is available as `$event`}) * * @example - <example> + <example name="ng-click"> <file name="index.html"> <button ng-click="count = count + 1" ng-init="count=0"> Increment @@ -25874,15 +26983,15 @@ forEach( return { restrict: 'A', compile: function($element, attr) { - // We expose the powerful $event object on the scope that provides access to the Window, - // etc. that isn't protected by the fast paths in $parse. We explicitly request better - // checks at the cost of speed since event handler expressions are not executed as - // frequently as regular change detection. - var fn = $parse(attr[directiveName], /* interceptorFn */ null, /* expensiveChecks */ true); + // NOTE: + // We expose the powerful `$event` object on the scope that provides access to the Window, + // etc. This is OK, because expressions are not sandboxed any more (and the expression + // sandbox was never meant to be a security feature anyway). + var fn = $parse(attr[directiveName]); return function ngEventHandler(scope, element) { element.on(eventName, function(event) { var callback = function() { - fn(scope, {$event:event}); + fn(scope, {$event: event}); }; if (forceAsyncEvents[eventName] && $rootScope.$$phase) { scope.$evalAsync(callback); @@ -25910,7 +27019,7 @@ forEach( * a dblclick. (The Event object is available as `$event`) * * @example - <example> + <example name="ng-dblclick"> <file name="index.html"> <button ng-dblclick="count = count + 1" ng-init="count=0"> Increment (on double click) @@ -25934,7 +27043,7 @@ forEach( * mousedown. ({@link guide/expression#-event- Event object is available as `$event`}) * * @example - <example> + <example name="ng-mousedown"> <file name="index.html"> <button ng-mousedown="count = count + 1" ng-init="count=0"> Increment (on mouse down) @@ -25958,7 +27067,7 @@ forEach( * mouseup. ({@link guide/expression#-event- Event object is available as `$event`}) * * @example - <example> + <example name="ng-mouseup"> <file name="index.html"> <button ng-mouseup="count = count + 1" ng-init="count=0"> Increment (on mouse up) @@ -25981,7 +27090,7 @@ forEach( * mouseover. ({@link guide/expression#-event- Event object is available as `$event`}) * * @example - <example> + <example name="ng-mouseover"> <file name="index.html"> <button ng-mouseover="count = count + 1" ng-init="count=0"> Increment (when mouse is over) @@ -26005,7 +27114,7 @@ forEach( * mouseenter. ({@link guide/expression#-event- Event object is available as `$event`}) * * @example - <example> + <example name="ng-mouseenter"> <file name="index.html"> <button ng-mouseenter="count = count + 1" ng-init="count=0"> Increment (when mouse enters) @@ -26029,7 +27138,7 @@ forEach( * mouseleave. ({@link guide/expression#-event- Event object is available as `$event`}) * * @example - <example> + <example name="ng-mouseleave"> <file name="index.html"> <button ng-mouseleave="count = count + 1" ng-init="count=0"> Increment (when mouse leaves) @@ -26053,7 +27162,7 @@ forEach( * mousemove. ({@link guide/expression#-event- Event object is available as `$event`}) * * @example - <example> + <example name="ng-mousemove"> <file name="index.html"> <button ng-mousemove="count = count + 1" ng-init="count=0"> Increment (when mouse moves) @@ -26077,7 +27186,7 @@ forEach( * keydown. (Event object is available as `$event` and can be interrogated for keyCode, altKey, etc.) * * @example - <example> + <example name="ng-keydown"> <file name="index.html"> <input ng-keydown="count = count + 1" ng-init="count=0"> key down count: {{count}} @@ -26099,7 +27208,7 @@ forEach( * keyup. (Event object is available as `$event` and can be interrogated for keyCode, altKey, etc.) * * @example - <example> + <example name="ng-keyup"> <file name="index.html"> <p>Typing in the input box below updates the key count</p> <input ng-keyup="count = count + 1" ng-init="count=0"> key up count: {{count}} @@ -26126,7 +27235,7 @@ forEach( * and can be interrogated for keyCode, altKey, etc.) * * @example - <example> + <example name="ng-keypress"> <file name="index.html"> <input ng-keypress="count = count + 1" ng-init="count=0"> key press count: {{count}} @@ -26159,7 +27268,7 @@ forEach( * ({@link guide/expression#-event- Event object is available as `$event`}) * * @example - <example module="submitExample"> + <example module="submitExample" name="ng-submit"> <file name="index.html"> <script> angular.module('submitExample', []) @@ -26255,7 +27364,7 @@ forEach( * copy. ({@link guide/expression#-event- Event object is available as `$event`}) * * @example - <example> + <example name="ng-copy"> <file name="index.html"> <input ng-copy="copied=true" ng-init="copied=false; value='copy me'" ng-model="value"> copied: {{copied}} @@ -26276,7 +27385,7 @@ forEach( * cut. ({@link guide/expression#-event- Event object is available as `$event`}) * * @example - <example> + <example name="ng-cut"> <file name="index.html"> <input ng-cut="cut=true" ng-init="cut=false; value='cut me'" ng-model="value"> cut: {{cut}} @@ -26297,7 +27406,7 @@ forEach( * paste. ({@link guide/expression#-event- Event object is available as `$event`}) * * @example - <example> + <example name="ng-paste"> <file name="index.html"> <input ng-paste="paste=true" ng-init="paste=false" placeholder='paste here'> pasted: {{paste}} @@ -26352,7 +27461,7 @@ forEach( * element is added to the DOM tree. * * @example - <example module="ngAnimate" deps="angular-animate.js" animations="true"> + <example module="ngAnimate" deps="angular-animate.js" animations="true" name="ng-if"> <file name="index.html"> <label>Click me: <input type="checkbox" ng-model="checked" ng-init="checked=true" /></label><br/> Show when checked: @@ -26420,8 +27529,8 @@ var ngIfDirective = ['$animate', '$compile', function($animate, $compile) { } if (block) { previousElements = getBlockNodes(block.clone); - $animate.leave(previousElements).then(function() { - previousElements = null; + $animate.leave(previousElements).done(function(response) { + if (response !== false) previousElements = null; }); block = null; } @@ -26482,7 +27591,7 @@ var ngIfDirective = ['$animate', '$compile', function($animate, $compile) { * - Otherwise enable scrolling only if the expression evaluates to truthy value. * * @example - <example module="includeExample" deps="angular-animate.js" animations="true"> + <example module="includeExample" deps="angular-animate.js" animations="true" name="ng-include"> <file name="index.html"> <div ng-controller="ExampleController"> <select ng-model="template" ng-options="t.name for t in templates"> @@ -26499,8 +27608,8 @@ var ngIfDirective = ['$animate', '$compile', function($animate, $compile) { angular.module('includeExample', ['ngAnimate']) .controller('ExampleController', ['$scope', function($scope) { $scope.templates = - [ { name: 'template1.html', url: 'template1.html'}, - { name: 'template2.html', url: 'template2.html'} ]; + [{ name: 'template1.html', url: 'template1.html'}, + { name: 'template2.html', url: 'template2.html'}]; $scope.template = $scope.templates[0]; }]); </file> @@ -26558,7 +27667,7 @@ var ngIfDirective = ['$animate', '$compile', function($animate, $compile) { }); it('should load template2.html', function() { - if (browser.params.browser == 'firefox') { + if (browser.params.browser === 'firefox') { // Firefox can't handle using selects // See https://github.com/angular/protractor/issues/480 return; @@ -26569,7 +27678,7 @@ var ngIfDirective = ['$animate', '$compile', function($animate, $compile) { }); it('should change to blank', function() { - if (browser.params.browser == 'firefox') { + if (browser.params.browser === 'firefox') { // Firefox can't handle using selects return; } @@ -26645,8 +27754,8 @@ var ngIncludeDirective = ['$templateRequest', '$anchorScroll', '$animate', currentScope = null; } if (currentElement) { - $animate.leave(currentElement).then(function() { - previousElement = null; + $animate.leave(currentElement).done(function(response) { + if (response !== false) previousElement = null; }); previousElement = currentElement; currentElement = null; @@ -26654,9 +27763,10 @@ var ngIncludeDirective = ['$templateRequest', '$anchorScroll', '$animate', }; scope.$watch(srcExp, function ngIncludeWatchAction(src) { - var afterAnimation = function() { - if (isDefined(autoScrollExp) && (!autoScrollExp || scope.$eval(autoScrollExp))) { - $anchorScroll(); + var afterAnimation = function(response) { + if (response !== false && isDefined(autoScrollExp) && + (!autoScrollExp || scope.$eval(autoScrollExp))) { + $anchorScroll(); } }; var thisChangeId = ++changeCounter; @@ -26679,7 +27789,7 @@ var ngIncludeDirective = ['$templateRequest', '$anchorScroll', '$animate', // directives to non existing elements. var clone = $transclude(newScope, function(clone) { cleanupLastIncludeContent(); - $animate.enter(clone, null, $element).then(afterAnimation); + $animate.enter(clone, null, $element).done(afterAnimation); }); currentScope = newScope; @@ -26767,7 +27877,7 @@ var ngIncludeFillContentDirective = ['$compile', * @param {expression} ngInit {@link guide/expression Expression} to eval. * * @example - <example module="initExample"> + <example module="initExample" name="ng-init"> <file name="index.html"> <script> angular.module('initExample', []) @@ -26895,9 +28005,7 @@ var ngListDirective = function() { 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 ngList = attr.ngList || ', '; var trimValues = attr.ngTrim !== 'false'; var separator = trimValues ? trim(ngList) : ngList; @@ -26939,15 +28047,19 @@ var ngListDirective = function() { DIRTY_CLASS: true, UNTOUCHED_CLASS: true, TOUCHED_CLASS: true, + PENDING_CLASS: true, + addSetValidityMethod: true, + setupValidity: true, + defaultModelOptions: false */ + var VALID_CLASS = 'ng-valid', INVALID_CLASS = 'ng-invalid', PRISTINE_CLASS = 'ng-pristine', DIRTY_CLASS = 'ng-dirty', UNTOUCHED_CLASS = 'ng-untouched', TOUCHED_CLASS = 'ng-touched', - PENDING_CLASS = 'ng-pending', EMPTY_CLASS = 'ng-empty', NOT_EMPTY_CLASS = 'ng-not-empty'; @@ -26960,32 +28072,57 @@ var ngModelMinErr = minErr('ngModel'); * @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.<Function>} $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. + * the control updates the ngModelController with a new {@link ngModel.NgModelController#$viewValue + `$viewValue`} from the DOM, usually via user input. + See {@link ngModel.NgModelController#$setViewValue `$setViewValue()`} for a detailed lifecycle explanation. + Note that the `$parsers` are not called when the bound ngModel expression changes programmatically. -Parsers are used to sanitize / convert the {@link ngModel.NgModelController#$viewValue -`$viewValue`}. + 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. -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`. + Parsers are used to sanitize / convert the {@link ngModel.NgModelController#$viewValue + `$viewValue`}. + + Returning `undefined` from a parser means a parse error occurred. In that case, + no {@link ngModel.NgModelController#$validators `$validators`} will run and the `ngModel` + will be set to `undefined` unless {@link ngModelOptions `ngModelOptions.allowInvalid`} + is set to `true`. The parse error is stored in `ngModel.$error.parse`. + + This simple example shows a parser that would convert text input value to lowercase: + * ```js + * function parse(value) { + * if (value) { + * return value.toLowerCase(); + * } + * } + * ngModelController.$parsers.push(parse); + * ``` * * @property {Array.<Function>} $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. + the bound ngModel expression changes programmatically. The `$formatters` are not called when the + value of the control is changed by user interaction. + + Formatters are used to format / convert the {@link ngModel.NgModelController#$modelValue + `$modelValue`} for display in the control. + + The functions are called in reverse array order, each passing the value through to the + next. The last return value is used as the actual DOM value. + + This simple example shows a formatter that would convert the model value to uppercase: + * ```js - * function formatter(value) { + * function format(value) { * if (value) { * return value.toUpperCase(); * } * } - * ngModel.$formatters.push(formatter); + * ngModel.$formatters.push(format); * ``` * * @property {Object.<string, function>} $validators A collection of validators that are applied @@ -27111,7 +28248,7 @@ is set to `true`. The parse error is stored in `ngModel.$error.parse`. var html = element.html(); // When we clear the content editable the browser leaves a <br> behind // If strip-br attribute is provided then we strip this out - if ( attrs.stripBr && html == '<br>' ) { + if (attrs.stripBr && html === '<br>') { html = ''; } ngModel.$setViewValue(html); @@ -27133,7 +28270,7 @@ is set to `true`. The parse error is stored in `ngModel.$error.parse`. </file> <file name="protractor.js" type="protractor"> it('should data-bind and become invalid', function() { - if (browser.params.browser == 'safari' || browser.params.browser == 'firefox') { + if (browser.params.browser === 'safari' || browser.params.browser === 'firefox') { // SafariDriver can't handle contenteditable // and Firefox driver can't clear contenteditables very well return; @@ -27153,8 +28290,8 @@ is set to `true`. The parse error is stored in `ngModel.$error.parse`. * * */ -var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', '$animate', '$timeout', '$rootScope', '$q', '$interpolate', - function($scope, $exceptionHandler, $attr, $element, $parse, $animate, $timeout, $rootScope, $q, $interpolate) { +NgModelController.$inject = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', '$animate', '$timeout', '$q', '$interpolate']; +function NgModelController($scope, $exceptionHandler, $attr, $element, $parse, $animate, $timeout, $q, $interpolate) { this.$viewValue = Number.NaN; this.$modelValue = Number.NaN; this.$$rawModelValue = undefined; // stores the parsed modelValue / model set from scope regardless of validity. @@ -27174,40 +28311,58 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ this.$pending = undefined; // keep pending keys here this.$name = $interpolate($attr.name || '', false)($scope); this.$$parentForm = nullFormCtrl; + this.$options = defaultModelOptions; + + this.$$parsedNgModel = $parse($attr.ngModel); + this.$$parsedNgModelAssign = this.$$parsedNgModel.assign; + this.$$ngModelGet = this.$$parsedNgModel; + this.$$ngModelSet = this.$$parsedNgModelAssign; + this.$$pendingDebounce = null; + this.$$parserValid = undefined; + + this.$$currentValidationRunId = 0; + + // https://github.com/angular/angular.js/issues/15833 + // Prevent `$$scope` from being iterated over by `copy` when NgModelController is deep watched + Object.defineProperty(this, '$$scope', {value: $scope}); + this.$$attr = $attr; + this.$$element = $element; + this.$$animate = $animate; + this.$$timeout = $timeout; + this.$$parse = $parse; + this.$$q = $q; + this.$$exceptionHandler = $exceptionHandler; + + setupValidity(this); + setupModelWatcher(this); +} + +NgModelController.prototype = { + $$initGetterSetters: function() { + if (this.$options.getOption('getterSetter')) { + var invokeModelGetter = this.$$parse(this.$$attr.ngModel + '()'), + invokeModelSetter = this.$$parse(this.$$attr.ngModel + '($$$p)'); - 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); + this.$$ngModelGet = function($scope) { + var modelValue = this.$$parsedNgModel($scope); if (isFunction(modelValue)) { modelValue = invokeModelGetter($scope); } return modelValue; }; - ngModelSet = function($scope, newValue) { - if (isFunction(parsedNgModel($scope))) { + this.$$ngModelSet = function($scope, newValue) { + if (isFunction(this.$$parsedNgModel($scope))) { invokeModelSetter($scope, {$$$p: newValue}); } else { - parsedNgModelAssign($scope, newValue); + this.$$parsedNgModelAssign($scope, newValue); } }; - } else if (!parsedNgModel.assign) { - throw ngModelMinErr('nonassign', "Expression '{0}' is non-assignable. Element: {1}", - $attr.ngModel, startingTag($element)); + } else if (!this.$$parsedNgModel.assign) { + throw ngModelMinErr('nonassign', 'Expression \'{0}\' is non-assignable. Element: {1}', + this.$$attr.ngModel, startingTag(this.$$element)); } - }; + }, + /** * @ngdoc method @@ -27229,7 +28384,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * 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; + $render: noop, /** * @ngdoc method @@ -27249,56 +28404,20 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * @param {*} value The value of the input to check for emptiness. * @returns {boolean} True if `value` is "empty". */ - this.$isEmpty = function(value) { + $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); + $$updateEmptyClasses: function(value) { + if (this.$isEmpty(value)) { + this.$$animate.removeClass(this.$$element, NOT_EMPTY_CLASS); + this.$$animate.addClass(this.$$element, EMPTY_CLASS); } else { - $animate.removeClass($element, EMPTY_CLASS); - $animate.addClass($element, NOT_EMPTY_CLASS); + this.$$animate.removeClass(this.$$element, EMPTY_CLASS); + this.$$animate.addClass(this.$$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 @@ -27311,12 +28430,12 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * 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); - }; + $setPristine: function() { + this.$dirty = false; + this.$pristine = true; + this.$$animate.removeClass(this.$$element, DIRTY_CLASS); + this.$$animate.addClass(this.$$element, PRISTINE_CLASS); + }, /** * @ngdoc method @@ -27329,13 +28448,13 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * 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(); - }; + $setDirty: function() { + this.$dirty = true; + this.$pristine = false; + this.$$animate.removeClass(this.$$element, PRISTINE_CLASS); + this.$$animate.addClass(this.$$element, DIRTY_CLASS); + this.$$parentForm.$setDirty(); + }, /** * @ngdoc method @@ -27349,11 +28468,11 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * 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); - }; + $setUntouched: function() { + this.$touched = false; + this.$untouched = true; + this.$$animate.setClass(this.$$element, UNTOUCHED_CLASS, TOUCHED_CLASS); + }, /** * @ngdoc method @@ -27366,11 +28485,11 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * 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); - }; + $setTouched: function() { + this.$touched = true; + this.$untouched = false; + this.$$animate.setClass(this.$$element, TOUCHED_CLASS, UNTOUCHED_CLASS); + }, /** * @ngdoc method @@ -27378,12 +28497,12 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * * @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 a some + * 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, you can have a situation where there is a period when - * the `$viewValue` is out of sync with the ngModel's `$modelValue`. + * 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. @@ -27401,10 +28520,10 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * angular.module('cancel-update-example', []) * * .controller('CancelUpdateController', ['$scope', function($scope) { - * $scope.model = {}; + * $scope.model = {value1: '', value2: ''}; * * $scope.setEmpty = function(e, value, rollback) { - * if (e.keyCode == 27) { + * if (e.keyCode === 27) { * e.preventDefault(); * if (rollback) { * $scope.myForm[value].$rollbackViewValue(); @@ -27416,8 +28535,8 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * </file> * <file name="index.html"> * <div ng-controller="CancelUpdateController"> - * <p>Both of these inputs are only updated if they are blurred. Hitting escape should - * empty them. Follow these steps and observe the difference:</p> + * <p>Both of these inputs are only updated if they are blurred. Hitting escape should + * empty them. Follow these steps and observe the difference:</p> * <ol> * <li>Type something in the input. You will see that the model is not yet updated</li> * <li>Press the Escape key. @@ -27434,17 +28553,17 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * * <form name="myForm" ng-model-options="{ updateOn: 'blur' }"> * <div> - * <p id="inputDescription1">Without $rollbackViewValue():</p> - * <input name="value1" aria-describedby="inputDescription1" ng-model="model.value1" - * ng-keydown="setEmpty($event, 'value1')"> - * value1: "{{ model.value1 }}" + * <p id="inputDescription1">Without $rollbackViewValue():</p> + * <input name="value1" aria-describedby="inputDescription1" ng-model="model.value1" + * ng-keydown="setEmpty($event, 'value1')"> + * value1: "{{ model.value1 }}" * </div> * * <div> - * <p id="inputDescription2">With $rollbackViewValue():</p> - * <input name="value2" aria-describedby="inputDescription2" ng-model="model.value2" - * ng-keydown="setEmpty($event, 'value2', true)"> - * value2: "{{ model.value2 }}" + * <p id="inputDescription2">With $rollbackViewValue():</p> + * <input name="value2" aria-describedby="inputDescription2" ng-model="model.value2" + * ng-keydown="setEmpty($event, 'value2', true)"> + * value2: "{{ model.value2 }}" * </div> * </form> * </div> @@ -27460,11 +28579,11 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ </file> * </example> */ - this.$rollbackViewValue = function() { - $timeout.cancel(pendingDebounce); - ctrl.$viewValue = ctrl.$$lastCommittedViewValue; - ctrl.$render(); - }; + $rollbackViewValue: function() { + this.$$timeout.cancel(this.$$pendingDebounce); + this.$viewValue = this.$$lastCommittedViewValue; + this.$render(); + }, /** * @ngdoc method @@ -27478,45 +28597,46 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * 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() { + $validate: function() { // ignore $validate before model is initialized - if (isNumber(ctrl.$modelValue) && isNaN(ctrl.$modelValue)) { + if (isNumberNaN(this.$modelValue)) { return; } - var viewValue = ctrl.$$lastCommittedViewValue; + var viewValue = this.$$lastCommittedViewValue; // Note: we use the $$rawModelValue as $modelValue might have been // set to undefined during a view -> model update that found validation // errors. We can't parse the view here, since that could change // the model although neither viewValue nor the model on the scope changed - var modelValue = ctrl.$$rawModelValue; + var modelValue = this.$$rawModelValue; - var prevValid = ctrl.$valid; - var prevModelValue = ctrl.$modelValue; + var prevValid = this.$valid; + var prevModelValue = this.$modelValue; - var allowInvalid = ctrl.$options && ctrl.$options.allowInvalid; + var allowInvalid = this.$options.getOption('allowInvalid'); - ctrl.$$runValidators(modelValue, viewValue, function(allValid) { + var that = this; + this.$$runValidators(modelValue, viewValue, function(allValid) { // If there was no change in validity, don't update the model // This prevents changing an invalid modelValue to undefined if (!allowInvalid && prevValid !== allValid) { - // Note: Don't check ctrl.$valid here, as we could have + // Note: Don't check this.$valid here, as we could have // external validators (e.g. calculated on the server), // that just call $setValidity and need the model value // to calculate their validity. - ctrl.$modelValue = allValid ? modelValue : undefined; + that.$modelValue = allValid ? modelValue : undefined; - if (ctrl.$modelValue !== prevModelValue) { - ctrl.$$writeModelToScope(); + if (that.$modelValue !== prevModelValue) { + that.$$writeModelToScope(); } } }); + }, - }; - - this.$$runValidators = function(modelValue, viewValue, doneCallback) { - currentValidationRunId++; - var localValidationRunId = currentValidationRunId; + $$runValidators: function(modelValue, viewValue, doneCallback) { + this.$$currentValidationRunId++; + var localValidationRunId = this.$$currentValidationRunId; + var that = this; // check parser error if (!processParseErrors()) { @@ -27530,34 +28650,34 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ processAsyncValidators(); function processParseErrors() { - var errorKey = ctrl.$$parserName || 'parse'; - if (isUndefined(parserValid)) { + var errorKey = that.$$parserName || 'parse'; + if (isUndefined(that.$$parserValid)) { setValidity(errorKey, null); } else { - if (!parserValid) { - forEach(ctrl.$validators, function(v, name) { + if (!that.$$parserValid) { + forEach(that.$validators, function(v, name) { setValidity(name, null); }); - forEach(ctrl.$asyncValidators, function(v, name) { + forEach(that.$asyncValidators, function(v, name) { setValidity(name, null); }); } // Set the parse error last, to prevent unsetting it, should a $validators key == parserName - setValidity(errorKey, parserValid); - return parserValid; + setValidity(errorKey, that.$$parserValid); + return that.$$parserValid; } return true; } function processSyncValidators() { var syncValidatorsValid = true; - forEach(ctrl.$validators, function(validator, name) { - var result = validator(modelValue, viewValue); + forEach(that.$validators, function(validator, name) { + var result = Boolean(validator(modelValue, viewValue)); syncValidatorsValid = syncValidatorsValid && result; setValidity(name, result); }); if (!syncValidatorsValid) { - forEach(ctrl.$asyncValidators, function(v, name) { + forEach(that.$asyncValidators, function(v, name) { setValidity(name, null); }); return false; @@ -27568,11 +28688,11 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ function processAsyncValidators() { var validatorPromises = []; var allValid = true; - forEach(ctrl.$asyncValidators, function(validator, name) { + forEach(that.$asyncValidators, function(validator, name) { var promise = validator(modelValue, viewValue); if (!isPromiseLike(promise)) { throw ngModelMinErr('nopromise', - "Expected asynchronous validator to return a promise but got '{0}' instead.", promise); + 'Expected asynchronous validator to return a promise but got \'{0}\' instead.', promise); } setValidity(name, undefined); validatorPromises.push(promise.then(function() { @@ -27585,25 +28705,25 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ if (!validatorPromises.length) { validationDone(true); } else { - $q.all(validatorPromises).then(function() { + that.$$q.all(validatorPromises).then(function() { validationDone(allValid); }, noop); } } function setValidity(name, isValid) { - if (localValidationRunId === currentValidationRunId) { - ctrl.$setValidity(name, isValid); + if (localValidationRunId === that.$$currentValidationRunId) { + that.$setValidity(name, isValid); } } function validationDone(allValid) { - if (localValidationRunId === currentValidationRunId) { + if (localValidationRunId === that.$$currentValidationRunId) { doneCallback(allValid); } } - }; + }, /** * @ngdoc method @@ -27616,84 +28736,87 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * 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; + $commitViewValue: function() { + var viewValue = this.$viewValue; - $timeout.cancel(pendingDebounce); + this.$$timeout.cancel(this.$$pendingDebounce); // If the view value has not changed then we should just exit, except in the case where there is // a native validator on the element. In this case the validation state may have changed even though // the viewValue has stayed empty. - if (ctrl.$$lastCommittedViewValue === viewValue && (viewValue !== '' || !ctrl.$$hasNativeValidators)) { + if (this.$$lastCommittedViewValue === viewValue && (viewValue !== '' || !this.$$hasNativeValidators)) { return; } - ctrl.$$updateEmptyClasses(viewValue); - ctrl.$$lastCommittedViewValue = viewValue; + this.$$updateEmptyClasses(viewValue); + this.$$lastCommittedViewValue = viewValue; // change to dirty - if (ctrl.$pristine) { + if (this.$pristine) { this.$setDirty(); } this.$$parseAndValidate(); - }; + }, - this.$$parseAndValidate = function() { - var viewValue = ctrl.$$lastCommittedViewValue; + $$parseAndValidate: function() { + var viewValue = this.$$lastCommittedViewValue; var modelValue = viewValue; - parserValid = isUndefined(modelValue) ? undefined : true; + var that = this; - if (parserValid) { - for (var i = 0; i < ctrl.$parsers.length; i++) { - modelValue = ctrl.$parsers[i](modelValue); + this.$$parserValid = isUndefined(modelValue) ? undefined : true; + + if (this.$$parserValid) { + for (var i = 0; i < this.$parsers.length; i++) { + modelValue = this.$parsers[i](modelValue); if (isUndefined(modelValue)) { - parserValid = false; + this.$$parserValid = false; break; } } } - if (isNumber(ctrl.$modelValue) && isNaN(ctrl.$modelValue)) { - // ctrl.$modelValue has not been touched yet... - ctrl.$modelValue = ngModelGet($scope); + if (isNumberNaN(this.$modelValue)) { + // this.$modelValue has not been touched yet... + this.$modelValue = this.$$ngModelGet(this.$$scope); } - var prevModelValue = ctrl.$modelValue; - var allowInvalid = ctrl.$options && ctrl.$options.allowInvalid; - ctrl.$$rawModelValue = modelValue; + var prevModelValue = this.$modelValue; + var allowInvalid = this.$options.getOption('allowInvalid'); + this.$$rawModelValue = modelValue; if (allowInvalid) { - ctrl.$modelValue = modelValue; + this.$modelValue = modelValue; writeToModelIfNeeded(); } // Pass the $$lastCommittedViewValue here, because the cached viewValue might be out of date. // This can happen if e.g. $setViewValue is called from inside a parser - ctrl.$$runValidators(modelValue, ctrl.$$lastCommittedViewValue, function(allValid) { + this.$$runValidators(modelValue, this.$$lastCommittedViewValue, function(allValid) { if (!allowInvalid) { - // Note: Don't check ctrl.$valid here, as we could have + // Note: Don't check this.$valid here, as we could have // external validators (e.g. calculated on the server), // that just call $setValidity and need the model value // to calculate their validity. - ctrl.$modelValue = allValid ? modelValue : undefined; + that.$modelValue = allValid ? modelValue : undefined; writeToModelIfNeeded(); } }); function writeToModelIfNeeded() { - if (ctrl.$modelValue !== prevModelValue) { - ctrl.$$writeModelToScope(); + if (that.$modelValue !== prevModelValue) { + that.$$writeModelToScope(); } } - }; + }, - this.$$writeModelToScope = function() { - ngModelSet($scope, ctrl.$modelValue); - forEach(ctrl.$viewChangeListeners, function(listener) { + $$writeModelToScope: function() { + this.$$ngModelSet(this.$$scope, this.$modelValue); + forEach(this.$viewChangeListeners, function(listener) { try { listener(); } catch (e) { - $exceptionHandler(e); + // eslint-disable-next-line no-invalid-this + this.$$exceptionHandler(e); } - }); - }; + }, this); + }, /** * @ngdoc method @@ -27709,9 +28832,10 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * * 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. + * value is sent directly for processing through the `$parsers` pipeline. After this, the `$validators` and + * `$asyncValidators` are called and the value is applied to `$modelValue`. + * Finally, the value is set to the **expression** specified in the `ng-model` attribute and + * all the registered change listeners, in the `$viewChangeListeners` list are called. * * In case the {@link ng.directive:ngModelOptions ngModelOptions} directive is used with `updateOn` * and the `default` trigger is not listed, all those actions will remain pending until one of the @@ -27745,43 +28869,62 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * @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); + $setViewValue: function(value, trigger) { + this.$viewValue = value; + if (this.$options.getOption('updateOnDefault')) { + this.$$debounceViewValueCommit(trigger); } - }; + }, - this.$$debounceViewValueCommit = function(trigger) { - var debounceDelay = 0, - options = ctrl.$options, - debounce; + $$debounceViewValueCommit: function(trigger) { + var debounceDelay = this.$options.getOption('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']; - } + if (isNumber(debounceDelay[trigger])) { + debounceDelay = debounceDelay[trigger]; + } else if (isNumber(debounceDelay['default'])) { + debounceDelay = debounceDelay['default']; } - $timeout.cancel(pendingDebounce); - if (debounceDelay) { - pendingDebounce = $timeout(function() { - ctrl.$commitViewValue(); + this.$$timeout.cancel(this.$$pendingDebounce); + var that = this; + if (debounceDelay > 0) { // this fails if debounceDelay is an object + this.$$pendingDebounce = this.$$timeout(function() { + that.$commitViewValue(); }, debounceDelay); - } else if ($rootScope.$$phase) { - ctrl.$commitViewValue(); + } else if (this.$$scope.$root.$$phase) { + this.$commitViewValue(); } else { - $scope.$apply(function() { - ctrl.$commitViewValue(); + this.$$scope.$apply(function() { + that.$commitViewValue(); }); } - }; + }, + + /** + * @ngdoc method + * + * @name ngModel.NgModelController#$overrideModelOptions + * + * @description + * + * Override the current model options settings programmatically. + * + * The previous `ModelOptions` value will not be modified. Instead, a + * new `ModelOptions` object will inherit from the previous one overriding + * or inheriting settings that are defined in the given parameter. + * + * See {@link ngModelOptions} for information about what options can be specified + * and how model option inheritance works. + * + * @param {Object} options a hash of settings to override the previous options + * + */ + $overrideModelOptions: function(options) { + this.$options = this.$options.createChild(options); + } +}; +function setupModelWatcher(ctrl) { // model -> value // Note: we cannot use a normal scope.$watch as we want to detect the following: // 1. scope value is 'a' @@ -27790,17 +28933,18 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ // -> 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); + ctrl.$$scope.$watch(function ngModelWatch(scope) { + var modelValue = ctrl.$$ngModelGet(scope); // if scope model value and ngModel value are out of sync // TODO(perf): why not move this to the action fn? if (modelValue !== ctrl.$modelValue && // checks for NaN is needed to allow setting the model to NaN when there's an asyncValidator + // eslint-disable-next-line no-self-compare (ctrl.$modelValue === ctrl.$modelValue || modelValue === modelValue) ) { ctrl.$modelValue = ctrl.$$rawModelValue = modelValue; - parserValid = undefined; + ctrl.$$parserValid = undefined; var formatters = ctrl.$formatters, idx = formatters.length; @@ -27814,13 +28958,46 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ ctrl.$viewValue = ctrl.$$lastCommittedViewValue = viewValue; ctrl.$render(); - ctrl.$$runValidators(modelValue, viewValue, noop); + // It is possible that model and view value have been updated during render + ctrl.$$runValidators(ctrl.$modelValue, ctrl.$viewValue, noop); } } return modelValue; }); -}]; +} + +/** + * @ngdoc method + * @name ngModel.NgModelController#$setValidity + * + * @description + * Change the validity state, and notify the form. + * + * This method can be called within $parsers/$formatters or a custom validation implementation. + * However, in most cases it should be sufficient to use the `ngModel.$validators` and + * `ngModel.$asyncValidators` collections which will call `$setValidity` automatically. + * + * @param {string} validationErrorKey Name of the validator. The `validationErrorKey` will be assigned + * to either `$error[validationErrorKey]` or `$pending[validationErrorKey]` + * (for unfulfilled `$asyncValidators`), so that it is available for data-binding. + * The `validationErrorKey` should be in camelCase and will get converted into dash-case + * for class name. Example: `myError` will result in `ng-valid-my-error` and `ng-invalid-my-error` + * 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({ + clazz: NgModelController, + set: function(object, property) { + object[property] = true; + }, + unset: function(object, property) { + delete object[property]; + } +}); /** @@ -27930,7 +29107,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * </pre> * * @example - * <example deps="angular-animate.js" animations="true" fixBase="true" module="inputExample"> + * <example deps="angular-animate.js" animations="true" fixBase="true" module="inputExample" name="ng-model"> <file name="index.html"> <script> angular.module('inputExample', []) @@ -28026,9 +29203,14 @@ var ngModelDirective = ['$rootScope', function($rootScope) { return { pre: function ngModelPreLink(scope, element, attr, ctrls) { var modelCtrl = ctrls[0], - formCtrl = ctrls[1] || modelCtrl.$$parentForm; + formCtrl = ctrls[1] || modelCtrl.$$parentForm, + optionsCtrl = ctrls[2]; - modelCtrl.$$setOptions(ctrls[2] && ctrls[2].$options); + if (optionsCtrl) { + modelCtrl.$options = optionsCtrl.$options; + } + + modelCtrl.$$initGetterSetters(); // notify others, especially parent forms formCtrl.$addControl(modelCtrl); @@ -28045,19 +29227,23 @@ var ngModelDirective = ['$rootScope', function($rootScope) { }, post: function ngModelPostLink(scope, element, attr, ctrls) { var modelCtrl = ctrls[0]; - if (modelCtrl.$options && modelCtrl.$options.updateOn) { - element.on(modelCtrl.$options.updateOn, function(ev) { + if (modelCtrl.$options.getOption('updateOn')) { + element.on(modelCtrl.$options.getOption('updateOn'), function(ev) { modelCtrl.$$debounceViewValueCommit(ev && ev.type); }); } + function setTouched() { + modelCtrl.$setTouched(); + } + element.on('blur', function() { if (modelCtrl.$touched) return; if ($rootScope.$$phase) { - scope.$evalAsync(modelCtrl.$setTouched); + scope.$evalAsync(setTouched); } else { - scope.$apply(modelCtrl.$setTouched); + scope.$apply(setTouched); } }); } @@ -28066,23 +29252,173 @@ var ngModelDirective = ['$rootScope', function($rootScope) { }; }]; +/* exported defaultModelOptions */ +var defaultModelOptions; var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/; /** + * @ngdoc type + * @name ModelOptions + * @description + * A container for the options set by the {@link ngModelOptions} directive + */ +function ModelOptions(options) { + this.$$options = options; +} + +ModelOptions.prototype = { + + /** + * @ngdoc method + * @name ModelOptions#getOption + * @param {string} name the name of the option to retrieve + * @returns {*} the value of the option + * @description + * Returns the value of the given option + */ + getOption: function(name) { + return this.$$options[name]; + }, + + /** + * @ngdoc method + * @name ModelOptions#createChild + * @param {Object} options a hash of options for the new child that will override the parent's options + * @return {ModelOptions} a new `ModelOptions` object initialized with the given options. + */ + createChild: function(options) { + var inheritAll = false; + + // make a shallow copy + options = extend({}, options); + + // Inherit options from the parent if specified by the value `"$inherit"` + forEach(options, /* @this */ function(option, key) { + if (option === '$inherit') { + if (key === '*') { + inheritAll = true; + } else { + options[key] = this.$$options[key]; + // `updateOn` is special so we must also inherit the `updateOnDefault` option + if (key === 'updateOn') { + options.updateOnDefault = this.$$options.updateOnDefault; + } + } + } else { + if (key === 'updateOn') { + // If the `updateOn` property contains the `default` event then we have to remove + // it from the event list and set the `updateOnDefault` flag. + options.updateOnDefault = false; + options[key] = trim(option.replace(DEFAULT_REGEXP, function() { + options.updateOnDefault = true; + return ' '; + })); + } + } + }, this); + + if (inheritAll) { + // We have a property of the form: `"*": "$inherit"` + delete options['*']; + defaults(options, this.$$options); + } + + // Finally add in any missing defaults + defaults(options, defaultModelOptions.$$options); + + return new ModelOptions(options); + } +}; + + +defaultModelOptions = new ModelOptions({ + updateOn: '', + updateOnDefault: true, + debounce: 0, + getterSetter: false, + allowInvalid: false, + timezone: null +}); + + +/** * @ngdoc directive * @name ngModelOptions * * @description - * 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. + * This directive allows you to modify the behaviour of {@link ngModel} directives within your + * application. You can specify an `ngModelOptions` directive on any element. All {@link ngModel} + * directives will use the options of their nearest `ngModelOptions` ancestor. + * + * The `ngModelOptions` settings are found by evaluating the value of the attribute directive as + * an Angular expression. This expression should evaluate to an object, whose properties contain + * the settings. For example: `<div "ng-model-options"="{ debounce: 100 }"`. + * + * ## Inheriting Options + * + * You can specify that an `ngModelOptions` setting should be inherited from a parent `ngModelOptions` + * directive by giving it the value of `"$inherit"`. + * Then it will inherit that setting from the first `ngModelOptions` directive found by traversing up the + * DOM tree. If there is no ancestor element containing an `ngModelOptions` directive then default settings + * will be used. + * + * For example given the following fragment of HTML + * + * + * ```html + * <div ng-model-options="{ allowInvalid: true, debounce: 200 }"> + * <form ng-model-options="{ updateOn: 'blur', allowInvalid: '$inherit' }"> + * <input ng-model-options="{ updateOn: 'default', allowInvalid: '$inherit' }" /> + * </form> + * </div> + * ``` + * + * the `input` element will have the following settings + * + * ```js + * { allowInvalid: true, updateOn: 'default', debounce: 0 } + * ``` + * + * Notice that the `debounce` setting was not inherited and used the default value instead. + * + * You can specify that all undefined settings are automatically inherited from an ancestor by + * including a property with key of `"*"` and value of `"$inherit"`. + * + * For example given the following fragment of HTML + * + * + * ```html + * <div ng-model-options="{ allowInvalid: true, debounce: 200 }"> + * <form ng-model-options="{ updateOn: 'blur', "*": '$inherit' }"> + * <input ng-model-options="{ updateOn: 'default', "*": '$inherit' }" /> + * </form> + * </div> + * ``` + * + * the `input` element will have the following settings + * + * ```js + * { allowInvalid: true, updateOn: 'default', debounce: 200 } + * ``` + * + * Notice that the `debounce` setting now inherits the value from the outer `<div>` element. + * + * If you are creating a reusable component then you should be careful when using `"*": "$inherit"` + * since you may inadvertently inherit a setting in the future that changes the behavior of your component. + * + * + * ## Triggering and debouncing model updates + * + * The `updateOn` and `debounce` properties allow you to specify a custom list of events that will + * trigger a model update and/or a debouncing delay so that the actual update only takes place when + * a timer expires; this timer will be reset after another change takes place. * * Given the nature of `ngModelOptions`, the value displayed inside input fields in the view might * be different from the value in the actual model. This means that if you update the model you - * should also invoke {@link ngModel.NgModelController `$rollbackViewValue`} on the relevant input field in + * 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`} + * 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. @@ -28091,271 +29427,194 @@ var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/; * `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. + * 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. * - * @param {Object} ngModelOptions options to apply to the current model. Valid keys are: + * <example name="ngModelOptions-directive-blur" module="optionsExample"> + * <file name="index.html"> + * <div ng-controller="ExampleController"> + * <form name="userForm"> + * <label> + * Name: + * <input type="text" name="userName" + * ng-model="user.name" + * ng-model-options="{ updateOn: 'blur' }" + * ng-keyup="cancel($event)" /> + * </label><br /> + * <label> + * Other data: + * <input type="text" ng-model="user.data" /> + * </label><br /> + * </form> + * <pre>user.name = <span ng-bind="user.name"></span></pre> + * </div> + * </file> + * <file name="app.js"> + * angular.module('optionsExample', []) + * .controller('ExampleController', ['$scope', function($scope) { + * $scope.user = { name: 'say', data: '' }; + * + * $scope.cancel = function(e) { + * if (e.keyCode === 27) { + * $scope.userForm.userName.$rollbackViewValue(); + * } + * }; + * }]); + * </file> + * <file name="protractor.js" type="protractor"> + * var model = element(by.binding('user.name')); + * var input = element(by.model('user.name')); + * var other = element(by.model('user.data')); + * + * it('should allow custom events', function() { + * input.sendKeys(' hello'); + * input.click(); + * expect(model.getText()).toEqual('say'); + * other.click(); + * expect(model.getText()).toEqual('say hello'); + * }); + * + * it('should $rollbackViewValue when model changes', function() { + * input.sendKeys(' hello'); + * expect(input.getAttribute('value')).toEqual('say hello'); + * input.sendKeys(protractor.Key.ESCAPE); + * expect(input.getAttribute('value')).toEqual('say'); + * other.click(); + * expect(model.getText()).toEqual('say'); + * }); + * </file> + * </example> + * + * The next example shows how to debounce model changes. Model will be updated only 1 sec after last change. + * If the `Clear` button is pressed, any debounced action is canceled and the value becomes empty. + * + * <example name="ngModelOptions-directive-debounce" module="optionsExample"> + * <file name="index.html"> + * <div ng-controller="ExampleController"> + * <form name="userForm"> + * Name: + * <input type="text" name="userName" + * ng-model="user.name" + * ng-model-options="{ debounce: 1000 }" /> + * <button ng-click="userForm.userName.$rollbackViewValue(); user.name=''">Clear</button><br /> + * </form> + * <pre>user.name = <span ng-bind="user.name"></span></pre> + * </div> + * </file> + * <file name="app.js"> + * angular.module('optionsExample', []) + * .controller('ExampleController', ['$scope', function($scope) { + * $scope.user = { name: 'say' }; + * }]); + * </file> + * </example> + * + * ## Model updates and validation + * + * The default behaviour in `ngModel` is that the model value is set to `undefined` when the + * validation determines that the value is invalid. By setting the `allowInvalid` property to true, + * the model will still be updated even if the value is invalid. + * + * + * ## Connecting to the scope + * + * By setting the `getterSetter` property to true you are telling ngModel that the `ngModel` expression + * on the scope refers to a "getter/setter" function rather than the value itself. + * + * The following example shows how to bind to getter/setters: + * + * <example name="ngModelOptions-directive-getter-setter" module="getterSetterExample"> + * <file name="index.html"> + * <div ng-controller="ExampleController"> + * <form name="userForm"> + * <label> + * Name: + * <input type="text" name="userName" + * ng-model="user.name" + * ng-model-options="{ getterSetter: true }" /> + * </label> + * </form> + * <pre>user.name = <span ng-bind="user.name()"></span></pre> + * </div> + * </file> + * <file name="app.js"> + * angular.module('getterSetterExample', []) + * .controller('ExampleController', ['$scope', function($scope) { + * var _name = 'Brian'; + * $scope.user = { + * name: function(newName) { + * return angular.isDefined(newName) ? (_name = newName) : _name; + * } + * }; + * }]); + * </file> + * </example> + * + * + * ## Specifying timezones + * + * You can specify the timezone that date/time input directives expect by providing its name in the + * `timezone` property. + * + * @param {Object} ngModelOptions options to apply to {@link ngModel} directives on this element and + * and its descendents. Valid keys are: * - `updateOn`: string specifying which event should the input be bound to. You can set several * events using an space delimited list. There is a special event called `default` that - * matches the default events belonging of the control. + * matches the default events belonging to the control. * - `debounce`: integer value which contains the debounce model update value in milliseconds. A * value of 0 triggers an immediate update. If an object is supplied instead, you can specify a * custom value for each event. For example: - * `ng-model-options="{ updateOn: 'default blur', debounce: { 'default': 500, 'blur': 0 } }"` + * ``` + * 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. + * `ngModel` as getters/setters. * - `timezone`: Defines the timezone to be used to read/write the `Date` instance in the model for - * `<input type="date">`, `<input type="time">`, ... . It understands UTC/GMT and the + * `<input type="date" />`, `<input type="time" />`, ... . 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. - - <example name="ngModelOptions-directive-blur" module="optionsExample"> - <file name="index.html"> - <div ng-controller="ExampleController"> - <form name="userForm"> - <label>Name: - <input type="text" name="userName" - ng-model="user.name" - ng-model-options="{ updateOn: 'blur' }" - ng-keyup="cancel($event)" /> - </label><br /> - <label>Other data: - <input type="text" ng-model="user.data" /> - </label><br /> - </form> - <pre>user.name = <span ng-bind="user.name"></span></pre> - <pre>user.data = <span ng-bind="user.data"></span></pre> - </div> - </file> - <file name="app.js"> - 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(); - } - }; - }]); - </file> - <file name="protractor.js" type="protractor"> - 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'); - }); - </file> - </example> - - 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. - - <example name="ngModelOptions-directive-debounce" module="optionsExample"> - <file name="index.html"> - <div ng-controller="ExampleController"> - <form name="userForm"> - <label>Name: - <input type="text" name="userName" - ng-model="user.name" - ng-model-options="{ debounce: 1000 }" /> - </label> - <button ng-click="userForm.userName.$rollbackViewValue(); user.name=''">Clear</button> - <br /> - </form> - <pre>user.name = <span ng-bind="user.name"></span></pre> - </div> - </file> - <file name="app.js"> - angular.module('optionsExample', []) - .controller('ExampleController', ['$scope', function($scope) { - $scope.user = { name: 'Igor' }; - }]); - </file> - </example> - - This one shows how to bind to getter/setters: - - <example name="ngModelOptions-directive-getter-setter" module="getterSetterExample"> - <file name="index.html"> - <div ng-controller="ExampleController"> - <form name="userForm"> - <label>Name: - <input type="text" name="userName" - ng-model="user.name" - ng-model-options="{ getterSetter: true }" /> - </label> - </form> - <pre>user.name = <span ng-bind="user.name()"></span></pre> - </div> - </file> - <file name="app.js"> - 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; - } - }; - }]); - </file> - </example> */ var ngModelOptionsDirective = function() { - return { - restrict: 'A', - controller: ['$scope', '$attrs', function($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; - } + NgModelOptionsController.$inject = ['$attrs', '$scope']; + function NgModelOptionsController($attrs, $scope) { + this.$$attrs = $attrs; + this.$$scope = $scope; } + NgModelOptionsController.prototype = { + $onInit: function() { + var parentOptions = this.parentCtrl ? this.parentCtrl.$options : defaultModelOptions; + var modelOptionsDefinition = this.$$scope.$eval(this.$$attrs.ngModelOptions); - 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; + this.$options = parentOptions.createChild(modelOptionsDefinition); } - } + }; - function toggleValidationCss(validationErrorKey, isValid) { - validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : ''; + return { + restrict: 'A', + // ngModelOptions needs to run before ngModel and input directives + priority: 10, + require: {parentCtrl: '?^^ngModelOptions'}, + bindToController: true, + controller: NgModelOptionsController + }; +}; - 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; - } +// shallow copy over values from `src` that are not already specified on `dst` +function defaults(dst, src) { + forEach(src, function(value, key) { + if (!isDefined(dst[key])) { + dst[key] = value; } - } - return true; + }); } /** @@ -28377,7 +29636,7 @@ function isObjectEmpty(obj) { * but the one wrapped in `ngNonBindable` is left alone. * * @example - <example> + <example name="ng-non-bindable"> <file name="index.html"> <div>Normal: {{1 + 2}}</div> <div ng-non-bindable>Ignored: {{1 + 2}}</div> @@ -28392,6 +29651,8 @@ function isObjectEmpty(obj) { */ var ngNonBindableDirective = ngDirective({ terminal: true, priority: 1000 }); +/* exported ngOptionsDirective */ + /* global jqLiteRemove */ var ngOptionsMinErr = minErr('ngOptions'); @@ -28407,13 +29668,12 @@ var ngOptionsMinErr = minErr('ngOptions'); * elements for the `<select>` element using the array or object obtained by evaluating the * `ngOptions` comprehension expression. * - * In many cases, `ngRepeat` can be used on `<option>` elements instead of `ngOptions` to achieve a - * similar result. However, `ngOptions` provides some benefits such as reducing memory and - * increasing speed by not creating a new scope for each repeated instance, as well as providing - * more flexibility in how the `<select>`'s model is assigned via the `select` **`as`** part of the - * comprehension expression. `ngOptions` should be used when the `<select>` model needs to be bound - * to a non-string value. This is because an option element can only be bound to string values at - * present. + * In many cases, {@link ng.directive:ngRepeat ngRepeat} can be used on `<option>` elements instead of + * `ngOptions` to achieve a similar result. However, `ngOptions` provides some benefits: + * - more flexibility in how the `<select>`'s model is assigned via the `select` **`as`** part of the + * comprehension expression + * - reduced memory consumption by not creating a new scope for each repeated instance + * - increased render speed by creating the options in a documentFragment instead of individually * * When an item in the `<select>` menu is selected, the array element or object property * represented by the selected option will be bound to the model identified by the `ngModel` @@ -28502,13 +29762,8 @@ var ngOptionsMinErr = minErr('ngOptions'); * is not matched against any `<option>` and the `<select>` appears as having no selected value. * * - * @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 The control is considered valid only if value is 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 {comprehension_expression=} ngOptions in one of the following forms: + * @param {string} ngModel Assignable AngularJS expression to data-bind to. + * @param {comprehension_expression} ngOptions in one of the following forms: * * * for array data sources: * * `label` **`for`** `value` **`in`** `array` @@ -28547,9 +29802,16 @@ var ngOptionsMinErr = minErr('ngOptions'); * used to identify the objects in the array. The `trackexpr` will most likely refer to the * `value` variable (e.g. `value.propertyName`). With this the selection is preserved * even when the options are recreated (e.g. reloaded from the server). + * @param {string=} name Property name of the form under which the control is published. + * @param {string=} required The control is considered valid only if value is 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=} ngAttrSize sets the size of the select element dynamically. Uses the + * {@link guide/interpolation#-ngattr-for-binding-to-arbitrary-attributes ngAttr} directive. * * @example - <example module="selectExample"> + <example module="selectExample" name="select"> <file name="index.html"> <script> angular.module('selectExample', []) @@ -28622,9 +29884,9 @@ var ngOptionsMinErr = minErr('ngOptions'); </example> */ -// jshint maxlen: false -// //00001111111111000000000002222222222000000000000000000000333333333300000000000000000000000004444444444400000000000005555555555555550000000006666666666666660000000777777777777777000000000000000888888888800000000000000000009999999999 -var NG_OPTIONS_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?(?:\s+disable\s+when\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$/; +/* eslint-disable max-len */ +// //00001111111111000000000002222222222000000000000000000000333333333300000000000000000000000004444444444400000000000005555555555555000000000666666666666600000007777777777777000000000000000888888888800000000000000000009999999999 +var NG_OPTIONS_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?(?:\s+disable\s+when\s+([\s\S]+?))?\s+for\s+(?:([$\w][$\w]*)|(?:\(\s*([$\w][$\w]*)\s*,\s*([$\w][$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$/; // 1: value expression (valueFn) // 2: label expression (displayFn) // 3: group by expression (groupByFn) @@ -28634,7 +29896,7 @@ var NG_OPTIONS_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s // 7: object item value variable name // 8: collection expression // 9: track by expression -// jshint maxlen: 100 +/* eslint-enable */ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile, $document, $parse) { @@ -28644,9 +29906,9 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile, var match = optionsExp.match(NG_OPTIONS_REGEXP); if (!(match)) { throw ngOptionsMinErr('iexp', - "Expected expression in form of " + - "'_select_ (as _label_)? for (_key_,)?_value_ in _collection_'" + - " but got '{0}'. Element: {1}", + 'Expected expression in form of ' + + '\'_select_ (as _label_)? for (_key_,)?_value_ in _collection_\'' + + ' but got \'{0}\'. Element: {1}', optionsExp, startingTag(selectElement)); } @@ -28788,7 +30050,7 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile, getViewValueFromOption: function(option) { // If the viewValue could be an object that may be mutated by the application, // we need to make a copy and not return the reference to the value on the option. - return trackBy ? angular.copy(option.viewValue) : option.viewValue; + return trackBy ? copy(option.viewValue) : option.viewValue; } }; } @@ -28809,15 +30071,15 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile, // The emptyOption allows the application developer to provide their own custom "empty" // option when the viewValue does not match any of the option values. - var emptyOption; for (var i = 0, children = selectElement.children(), ii = children.length; i < ii; i++) { if (children[i].value === '') { - emptyOption = children.eq(i); + selectCtrl.hasEmptyOption = true; + selectCtrl.emptyOption = children.eq(i); break; } } - var providedEmptyOption = !!emptyOption; + var providedEmptyOption = !!selectCtrl.emptyOption; var unknownOption = jqLite(optionTemplate.cloneNode(false)); unknownOption.val('?'); @@ -28829,39 +30091,22 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile, // we only need to create it once. var listFragment = $document[0].createDocumentFragment(); - var renderEmptyOption = function() { - if (!providedEmptyOption) { - selectElement.prepend(emptyOption); - } - selectElement.val(''); - emptyOption.prop('selected', true); // needed for IE - emptyOption.attr('selected', true); - }; - - var removeEmptyOption = function() { - if (!providedEmptyOption) { - emptyOption.remove(); - } - }; - - - var renderUnknownOption = function() { - selectElement.prepend(unknownOption); - selectElement.val('?'); - unknownOption.prop('selected', true); // needed for IE - unknownOption.attr('selected', true); - }; - - var removeUnknownOption = function() { - unknownOption.remove(); + // Overwrite the implementation. ngOptions doesn't use hashes + selectCtrl.generateUnknownOptionValue = function(val) { + return '?'; }; // Update the controller methods for multiple selectable options if (!multiple) { selectCtrl.writeValue = function writeNgOptionsValue(value) { + var selectedOption = options.selectValueMap[selectElement.val()]; var option = options.getOptionFromViewValue(value); + // Make sure to remove the selected attribute from the previously selected option + // Otherwise, screen readers might get confused + if (selectedOption) selectedOption.element.removeAttribute('selected'); + if (option) { // Don't update the option when it is already selected. // For example, the browser will select the first option by default. In that case, @@ -28869,8 +30114,8 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile, // set always if (selectElement[0].value !== option.selectValue) { - removeUnknownOption(); - removeEmptyOption(); + selectCtrl.removeUnknownOption(); + selectCtrl.unselectEmptyOption(); selectElement[0].value = option.selectValue; option.element.selected = true; @@ -28878,12 +30123,13 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile, option.element.setAttribute('selected', 'selected'); } else { - if (value === null || providedEmptyOption) { - removeUnknownOption(); - renderEmptyOption(); + + if (providedEmptyOption) { + selectCtrl.selectEmptyOption(); + } else if (selectCtrl.unknownOption.parent().length) { + selectCtrl.updateUnknownOption(value); } else { - removeEmptyOption(); - renderUnknownOption(); + selectCtrl.renderUnknownOption(value); } } }; @@ -28893,8 +30139,8 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile, var selectedOption = options.selectValueMap[selectElement.val()]; if (selectedOption && !selectedOption.disabled) { - removeEmptyOption(); - removeUnknownOption(); + selectCtrl.unselectEmptyOption(); + selectCtrl.removeUnknownOption(); return options.getViewValueFromOption(selectedOption); } return null; @@ -28902,6 +30148,7 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile, // If we are using `track by` then we must watch the tracked value on the model // since ngModel only watches for object identity change + // FIXME: When a user selects an option, this watch will fire needlessly if (ngOptions.trackBy) { scope.$watch( function() { return ngOptions.getTrackByValue(ngModelCtrl.$viewValue); }, @@ -28911,22 +30158,17 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile, } else { - ngModelCtrl.$isEmpty = function(value) { - return !value || value.length === 0; - }; + selectCtrl.writeValue = function writeNgOptionsMultiple(values) { + // Only set `<option>.selected` if necessary, in order to prevent some browsers from + // scrolling to `<option>` elements that are outside the `<select>` element's viewport. + var selectedOptions = values && values.map(getAndUpdateSelectedOption) || []; - selectCtrl.writeValue = function writeNgOptionsMultiple(value) { options.items.forEach(function(option) { - option.element.selected = false; + if (option.element.selected && !includes(selectedOptions, option)) { + option.element.selected = false; + } }); - - if (value) { - value.forEach(function(item) { - var option = options.getOptionFromViewValue(item); - if (option) option.element.selected = true; - }); - } }; @@ -28959,21 +30201,44 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile, } } - if (providedEmptyOption) { // we need to remove it before calling selectElement.empty() because otherwise IE will // remove the label from the element. wtf? - emptyOption.remove(); + selectCtrl.emptyOption.remove(); // compile the element since there might be bindings in it - $compile(emptyOption)(scope); + $compile(selectCtrl.emptyOption)(scope); + + if (selectCtrl.emptyOption[0].nodeType === NODE_TYPE_COMMENT) { + // This means the empty option has currently no actual DOM node, probably because + // it has been modified by a transclusion directive. + selectCtrl.hasEmptyOption = false; + + // Redefine the registerOption function, which will catch + // options that are added by ngIf etc. (rendering of the node is async because of + // lazy transclusion) + selectCtrl.registerOption = function(optionScope, optionEl) { + if (optionEl.val() === '') { + selectCtrl.hasEmptyOption = true; + selectCtrl.emptyOption = optionEl; + selectCtrl.emptyOption.removeClass('ng-scope'); + // This ensures the new empty option is selected if previously no option was selected + ngModelCtrl.$render(); + + optionEl.on('$destroy', function() { + selectCtrl.hasEmptyOption = false; + selectCtrl.emptyOption = undefined; + }); + } + }; + + } else { + // remove the class, which is added automatically because we recompile the element and it + // becomes the compilation root + selectCtrl.emptyOption.removeClass('ng-scope'); + } - // remove the class, which is added automatically because we recompile the element and it - // becomes the compilation root - emptyOption.removeClass('ng-scope'); - } else { - emptyOption = jqLite(optionTemplate.cloneNode(false)); } selectElement.empty(); @@ -28993,6 +30258,14 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile, updateOptionElement(option, optionElement); } + function getAndUpdateSelectedOption(viewValue) { + var option = options.getOptionFromViewValue(viewValue); + var element = option && option.element; + + if (element && !element.selected) element.selected = true; + + return option; + } function updateOptionElement(option, element) { option.element = element; @@ -29006,7 +30279,7 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile, element.label = option.label; element.textContent = option.label; } - if (option.value !== element.value) element.value = option.selectValue; + element.value = option.selectValue; } function updateOptions() { @@ -29035,7 +30308,7 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile, // Ensure that the empty option is always there if it was explicitly provided if (providedEmptyOption) { - selectElement.prepend(emptyOption); + selectElement.prepend(selectCtrl.emptyOption); } options.items.forEach(function addOption(option) { @@ -29194,7 +30467,7 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile, * @param {number=} offset Offset to deduct from the total number. * * @example - <example module="pluralizeExample"> + <example module="pluralizeExample" name="ng-pluralize"> <file name="index.html"> <script> angular.module('pluralizeExample', []) @@ -29308,7 +30581,7 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale, scope.$watch(numberExp, function ngPluralizeWatchAction(newVal) { var count = parseFloat(newVal); - var countIsNaN = isNaN(count); + var countIsNaN = isNumberNaN(count); if (!countIsNaN && !(count in whens)) { // If an explicit number rule such as 1, 2, 3... is defined, just use it. @@ -29318,12 +30591,12 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale, // 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 && isNumber(lastCount) && isNaN(lastCount))) { + 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); + $log.debug('ngPluralize: no rule defined for \'' + count + '\' in ' + whenExp); } watchRemover = noop; updateElementText(); @@ -29341,10 +30614,13 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale, }; }]; +/* exported ngRepeatDirective */ + /** * @ngdoc directive * @name ngRepeat * @multiElement + * @restrict A * * @description * The `ngRepeat` directive instantiates a template once per item from a collection. Each template @@ -29377,7 +30653,7 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale, * <div ng-repeat="(key, value) in myObj"> ... </div> * ``` * - * However, there are a limitations compared to array iteration: + * 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 @@ -29401,7 +30677,7 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale, * # 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: + * 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. @@ -29409,7 +30685,7 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale, * * 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 + * 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 @@ -29436,19 +30712,29 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale, * ``` * * <div class="alert alert-success"> - * If you are working with objects that have an identifier property, you should track - * by the identifier instead of the whole object. Should you reload your data later, `ngRepeat` + * 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. * </div> + * * ```html * <div ng-repeat="model in collection track by model.id"> * {{model.name}} * </div> * ``` * + * <br /> + * <div class="alert alert-warning"> + * 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. + * </div> + * * 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 @@ -29457,15 +30743,17 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale, * </div> * ``` * + * <br /> * <div class="alert alert-warning"> * **Note:** `track by` must always be the last expression: * </div> * ``` - * <div ng-repeat="model in collection | orderBy: 'id' as filtered_result track by model.id"> - * {{model.name}} - * </div> + * <div ng-repeat="model in collection | orderBy: 'id' as filtered_result track by model.id"> + * {{model.name}} + * </div> * ``` * + * * # 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. @@ -29572,8 +30860,8 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale, * * @example * This example uses `ngRepeat` to display a list of people. A filter is used to restrict the displayed - * results by name. New (entering) and removed (leaving) items are animated. - <example module="ngRepeat" name="ngRepeat" deps="angular-animate.js" animations="true"> + * results by name or by age. New (entering) and removed (leaving) items are animated. + <example module="ngRepeat" name="ngRepeat" deps="angular-animate.js" animations="true" name="ng-repeat"> <file name="index.html"> <div ng-controller="repeatController"> I have {{friends.length}} friends. They are: @@ -29582,7 +30870,7 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale, <li class="animate-repeat" ng-repeat="friend in friends | filter:q as results"> [{{$index + 1}}] {{friend.name}} who is {{friend.age}} years old. </li> - <li class="animate-repeat" ng-if="results.length == 0"> + <li class="animate-repeat" ng-if="results.length === 0"> <strong>No results found...</strong> </li> </ul> @@ -29675,9 +30963,8 @@ var ngRepeatDirective = ['$parse', '$animate', '$compile', function($parse, $ani scope.$first = (index === 0); scope.$last = (index === (arrayLength - 1)); scope.$middle = !(scope.$first || scope.$last); - // jshint bitwise: false - scope.$odd = !(scope.$even = (index&1) === 0); - // jshint bitwise: true + // eslint-disable-next-line no-bitwise + scope.$odd = !(scope.$even = (index & 1) === 0); }; var getBlockStart = function(block) { @@ -29703,7 +30990,7 @@ var ngRepeatDirective = ['$parse', '$animate', '$compile', function($parse, $ani 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}'.", + throw ngRepeatMinErr('iexp', 'Expected expression in form of \'_item_ in _collection_[ track by _id_]\' but got \'{0}\'.', expression); } @@ -29712,10 +30999,10 @@ var ngRepeatDirective = ['$parse', '$animate', '$compile', function($parse, $ani var aliasAs = match[3]; var trackByExp = match[4]; - match = lhs.match(/^(?:(\s*[\$\w]+)|\(\s*([\$\w]+)\s*,\s*([\$\w]+)\s*\))$/); + 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}'.", + 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]; @@ -29723,7 +31010,7 @@ var ngRepeatDirective = ['$parse', '$animate', '$compile', function($parse, $ani 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.", + throw ngRepeatMinErr('badident', 'alias \'{0}\' is invalid --- must be a valid JS identifier which is not a reserved name.', aliasAs); } @@ -29819,7 +31106,7 @@ var ngRepeatDirective = ['$parse', '$animate', '$compile', function($parse, $ani 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}", + '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 @@ -29860,7 +31147,7 @@ var ngRepeatDirective = ['$parse', '$animate', '$compile', function($parse, $ani nextNode = nextNode.nextSibling; } while (nextNode && nextNode[NG_REMOVED]); - if (getBlockStart(block) != nextNode) { + if (getBlockStart(block) !== nextNode) { // existing item which got moved $animate.move(getBlockNodes(block.clone), null, previousNode); } @@ -29900,11 +31187,13 @@ var NG_HIDE_IN_PROGRESS_CLASS = 'ng-hide-animate'; * @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}). + * 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 $scope.myValue is truthy (element is visible) --> @@ -29914,31 +31203,32 @@ var NG_HIDE_IN_PROGRESS_CLASS = 'ng-hide-animate'; * <div ng-show="myValue" class="ng-hide"></div> * ``` * - * 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. + * 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? + * ## 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. + * 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. + * 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. + * 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`, you can simply overwrite the styles for + * the `.ng-hide` CSS class. 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 */ + * /* These are just alternative ways of hiding an element */ * display: block!important; * position: absolute; * top: -9999px; @@ -29946,29 +31236,20 @@ var NG_HIDE_IN_PROGRESS_CLASS = 'ng-hide-animate'; * } * ``` * - * By default you don't need to override in CSS anything and the animations will work around the display style. + * By default you don't need to override anything in CSS 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. + * 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 the elements are not actually hidden during the animation. * * ```css - * // - * //a working example can be found at the bottom of this page - * // + * /* 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; + * transition: all 0.5s linear; * } * * .my-element.ng-hide-add { ... } @@ -29977,76 +31258,108 @@ var NG_HIDE_IN_PROGRESS_CLASS = 'ng-hide-animate'; * .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. + * 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 automatically handle the style toggling 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 | + * | 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. + * @param {expression} ngShow If the {@link guide/expression expression} is truthy/falsy then the + * element is shown/hidden respectively. * * @example - <example module="ngAnimate" deps="angular-animate.js" animations="true"> + * A simple example, animating the element's opacity: + * + <example module="ngAnimate" deps="angular-animate.js" animations="true" name="ng-show-simple"> <file name="index.html"> - Click me: <input type="checkbox" ng-model="checked" aria-label="Toggle ngHide"><br/> - <div> - Show: - <div class="check-element animate-show" ng-show="checked"> - <span class="glyphicon glyphicon-thumbs-up"></span> I show up when your checkbox is checked. - </div> - </div> - <div> - Hide: - <div class="check-element animate-show" ng-hide="checked"> - <span class="glyphicon glyphicon-thumbs-down"></span> I hide when your checkbox is checked. - </div> + Show: <input type="checkbox" ng-model="checked" aria-label="Toggle ngShow"><br /> + <div class="check-element animate-show-hide" ng-show="checked"> + I show up when your checkbox is checked. </div> </file> - <file name="glyphicons.css"> - @import url(../../components/bootstrap-3.1.1/css/bootstrap.css); - </file> <file name="animations.css"> - .animate-show { - line-height: 20px; + .animate-show-hide.ng-hide { + opacity: 0; + } + + .animate-show-hide.ng-hide-add, + .animate-show-hide.ng-hide-remove { + transition: all linear 0.5s; + } + + .check-element { + border: 1px solid black; opacity: 1; padding: 10px; - border: 1px solid black; - background: white; } + </file> + <file name="protractor.js" type="protractor"> + it('should check ngShow', function() { + var checkbox = element(by.model('checked')); + var checkElem = element(by.css('.check-element')); - .animate-show.ng-hide-add, .animate-show.ng-hide-remove { - transition: all linear 0.5s; + expect(checkElem.isDisplayed()).toBe(false); + checkbox.click(); + expect(checkElem.isDisplayed()).toBe(true); + }); + </file> + </example> + * + * <hr /> + * @example + * A more complex example, featuring different show/hide animations: + * + <example module="ngAnimate" deps="angular-animate.js" animations="true" name="ng-show-complex"> + <file name="index.html"> + Show: <input type="checkbox" ng-model="checked" aria-label="Toggle ngShow"><br /> + <div class="check-element funky-show-hide" ng-show="checked"> + I show up when your checkbox is checked. + </div> + </file> + <file name="animations.css"> + body { + overflow: hidden; + perspective: 1000px; } - .animate-show.ng-hide { - line-height: 0; - opacity: 0; - padding: 0 10px; + .funky-show-hide.ng-hide-add { + transform: rotateZ(0); + transform-origin: right; + transition: all 0.5s ease-in-out; + } + + .funky-show-hide.ng-hide-add.ng-hide-add-active { + transform: rotateZ(-135deg); + } + + .funky-show-hide.ng-hide-remove { + transform: rotateY(90deg); + transform-origin: left; + transition: all 0.5s ease; + } + + .funky-show-hide.ng-hide-remove.ng-hide-remove-active { + transform: rotateY(0); } .check-element { - padding: 10px; border: 1px solid black; - background: white; + opacity: 1; + padding: 10px; } </file> <file name="protractor.js" type="protractor"> - 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(); + it('should check ngShow', function() { + var checkbox = element(by.model('checked')); + var checkElem = element(by.css('.check-element')); - element(by.model('checked')).click(); - - expect(thumbsUp.isDisplayed()).toBeTruthy(); - expect(thumbsDown.isDisplayed()).toBeFalsy(); + expect(checkElem.isDisplayed()).toBe(false); + checkbox.click(); + expect(checkElem.isDisplayed()).toBe(true); }); </file> </example> @@ -30076,11 +31389,13 @@ var ngShowDirective = ['$animate', function($animate) { * @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}). + * 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 $scope.myValue is truthy (element is hidden) --> @@ -30090,30 +31405,32 @@ var ngShowDirective = ['$animate', function($animate) { * <div ng-hide="myValue"></div> * ``` * - * 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. + * 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? + * ## 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. + * 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. + * 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: + * 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`, you can simply overwrite the styles for + * the `.ng-hide` CSS class. 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 { - * /* this is just another form of hiding an element */ + * .ng-hide:not(.ng-hide-animate) { + * /* These are just alternative ways of hiding an element */ * display: block!important; * position: absolute; * top: -9999px; @@ -30121,20 +31438,20 @@ var ngShowDirective = ['$animate', function($animate) { * } * ``` * - * By default you don't need to override in CSS anything and the animations will work around the display style. + * 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. + * 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 the elements are not actually hidden during the animation. * * ```css - * // - * //a working example can be found at the bottom of this page - * // + * /* 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; + * transition: all 0.5s linear; * } * * .my-element.ng-hide-add { ... } @@ -30143,74 +31460,109 @@ var ngShowDirective = ['$animate', function($animate) { * .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. + * 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 automatically handle the style toggling 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 | + * | 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. + * @param {expression} ngHide If the {@link guide/expression expression} is truthy/falsy then the + * element is hidden/shown respectively. * * @example - <example module="ngAnimate" deps="angular-animate.js" animations="true"> + * A simple example, animating the element's opacity: + * + <example module="ngAnimate" deps="angular-animate.js" animations="true" name="ng-hide-simple"> <file name="index.html"> - Click me: <input type="checkbox" ng-model="checked" aria-label="Toggle ngShow"><br/> - <div> - Show: - <div class="check-element animate-hide" ng-show="checked"> - <span class="glyphicon glyphicon-thumbs-up"></span> I show up when your checkbox is checked. - </div> + Hide: <input type="checkbox" ng-model="checked" aria-label="Toggle ngHide"><br /> + <div class="check-element animate-show-hide" ng-hide="checked"> + I hide when your checkbox is checked. </div> - <div> - Hide: - <div class="check-element animate-hide" ng-hide="checked"> - <span class="glyphicon glyphicon-thumbs-down"></span> I hide when your checkbox is checked. - </div> - </div> - </file> - <file name="glyphicons.css"> - @import url(../../components/bootstrap-3.1.1/css/bootstrap.css); </file> <file name="animations.css"> - .animate-hide { + .animate-show-hide.ng-hide { + opacity: 0; + } + + .animate-show-hide.ng-hide-add, + .animate-show-hide.ng-hide-remove { transition: all linear 0.5s; - line-height: 20px; + } + + .check-element { + border: 1px solid black; opacity: 1; padding: 10px; - border: 1px solid black; - background: white; } + </file> + <file name="protractor.js" type="protractor"> + it('should check ngHide', function() { + var checkbox = element(by.model('checked')); + var checkElem = element(by.css('.check-element')); - .animate-hide.ng-hide { - line-height: 0; - opacity: 0; - padding: 0 10px; + expect(checkElem.isDisplayed()).toBe(true); + checkbox.click(); + expect(checkElem.isDisplayed()).toBe(false); + }); + </file> + </example> + * + * <hr /> + * @example + * A more complex example, featuring different show/hide animations: + * + <example module="ngAnimate" deps="angular-animate.js" animations="true" name="ng-hide-complex"> + <file name="index.html"> + Hide: <input type="checkbox" ng-model="checked" aria-label="Toggle ngHide"><br /> + <div class="check-element funky-show-hide" ng-hide="checked"> + I hide when your checkbox is checked. + </div> + </file> + <file name="animations.css"> + body { + overflow: hidden; + perspective: 1000px; + } + + .funky-show-hide.ng-hide-add { + transform: rotateZ(0); + transform-origin: right; + transition: all 0.5s ease-in-out; + } + + .funky-show-hide.ng-hide-add.ng-hide-add-active { + transform: rotateZ(-135deg); + } + + .funky-show-hide.ng-hide-remove { + transform: rotateY(90deg); + transform-origin: left; + transition: all 0.5s ease; + } + + .funky-show-hide.ng-hide-remove.ng-hide-remove-active { + transform: rotateY(0); } .check-element { - padding: 10px; border: 1px solid black; - background: white; + opacity: 1; + padding: 10px; } </file> <file name="protractor.js" type="protractor"> - 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(); + it('should check ngHide', function() { + var checkbox = element(by.model('checked')); + var checkElem = element(by.css('.check-element')); - element(by.model('checked')).click(); - - expect(thumbsUp.isDisplayed()).toBeTruthy(); - expect(thumbsDown.isDisplayed()).toBeFalsy(); + expect(checkElem.isDisplayed()).toBe(true); + checkbox.click(); + expect(checkElem.isDisplayed()).toBe(false); }); </file> </example> @@ -30255,7 +31607,7 @@ var ngHideDirective = ['$animate', function($animate) { * See the 'background-color' style in the example below. * * @example - <example> + <example name="ng-style"> <file name="index.html"> <input type="button" value="set color" ng-click="myStyle={color:'red'}"> <input type="button" value="set background" ng-click="myStyle={'background-color':'blue'}"> @@ -30341,14 +31693,18 @@ var ngStyleDirective = ngDirective(function(scope, element, attr) { * * * `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. + * 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 - <example module="switchExample" deps="angular-animate.js" animations="true"> + <example module="switchExample" deps="angular-animate.js" animations="true" name="ng-switch"> <file name="index.html"> <div ng-controller="ExampleController"> <select ng-model="selection" ng-options="item for item in items"> @@ -30357,7 +31713,7 @@ var ngStyleDirective = ngDirective(function(scope, element, attr) { <hr/> <div class="animate-switch-container" ng-switch on="selection"> - <div class="animate-switch" ng-switch-when="settings">Settings Div</div> + <div class="animate-switch" ng-switch-when="settings|options" ng-switch-when-separator="|">Settings Div</div> <div class="animate-switch" ng-switch-when="home">Home Span</div> <div class="animate-switch" ng-switch-default>default</div> </div> @@ -30366,7 +31722,7 @@ var ngStyleDirective = ngDirective(function(scope, element, attr) { <file name="script.js"> angular.module('switchExample', ['ngAnimate']) .controller('ExampleController', ['$scope', function($scope) { - $scope.items = ['settings', 'home', 'other']; + $scope.items = ['settings', 'home', 'options', 'other']; $scope.selection = $scope.items[0]; }]); </file> @@ -30413,8 +31769,12 @@ var ngStyleDirective = ngDirective(function(scope, element, attr) { select.all(by.css('option')).get(1).click(); expect(switchElem.getText()).toMatch(/Home Span/); }); - it('should select default', function() { + 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/); }); </file> @@ -30425,7 +31785,7 @@ var ngSwitchDirective = ['$animate', '$compile', function($animate, $compile) { require: 'ngSwitch', // asks for $scope to fool the BC controller module - controller: ['$scope', function ngSwitchController() { + controller: ['$scope', function NgSwitchController() { this.cases = {}; }], link: function(scope, element, attr, ngSwitchController) { @@ -30436,21 +31796,24 @@ var ngSwitchDirective = ['$animate', '$compile', function($animate, $compile) { selectedScopes = []; var spliceFactory = function(array, index) { - return function() { array.splice(index, 1); }; + return function(response) { + if (response !== false) array.splice(index, 1); + }; }; scope.$watch(watchExpr, function ngSwitchWatchAction(value) { var i, ii; - for (i = 0, ii = previousLeaveAnimations.length; i < ii; ++i) { - $animate.cancel(previousLeaveAnimations[i]); + + // Start with the last, in case the array is modified during the loop + while (previousLeaveAnimations.length) { + $animate.cancel(previousLeaveAnimations.pop()); } - previousLeaveAnimations.length = 0; for (i = 0, ii = selectedScopes.length; i < ii; ++i) { var selected = getBlockNodes(selectedElements[i].clone); selectedScopes[i].$destroy(); - var promise = previousLeaveAnimations[i] = $animate.leave(selected); - promise.then(spliceFactory(previousLeaveAnimations, i)); + var runner = previousLeaveAnimations[i] = $animate.leave(selected); + runner.done(spliceFactory(previousLeaveAnimations, i)); } selectedElements.length = 0; @@ -30480,8 +31843,16 @@ var ngSwitchWhenDirective = ngDirective({ require: '^ngSwitch', multiElement: true, link: function(scope, element, attrs, ctrl, $transclude) { - ctrl.cases['!' + attrs.ngSwitchWhen] = (ctrl.cases['!' + attrs.ngSwitchWhen] || []); - ctrl.cases['!' + attrs.ngSwitchWhen].push({ transclude: $transclude, element: element }); + + 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 }); + }); } }); @@ -30509,8 +31880,8 @@ var ngSwitchDefaultDirective = ngDirective({ * * 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. + * If the transcluded content is empty (or only whitespace), the existing content is left intact. This lets you provide fallback + * content in the case that no transcluded content is provided. * * @element ANY * @@ -30543,7 +31914,7 @@ var ngSwitchDefaultDirective = ngDirective({ * <div ng-controller="ExampleController"> * <input ng-model="title" aria-label="title"> <br/> * <textarea ng-model="text" aria-label="text"></textarea> <br/> - * <pane title="{{title}}">{{text}}</pane> + * <pane title="{{title}}"><span>{{text}}</span></pane> * </div> * </file> * <file name="protractor.js" type="protractor"> @@ -30565,7 +31936,7 @@ var ngSwitchDefaultDirective = ngDirective({ * This example shows how to use `NgTransclude` with fallback content, that * is displayed if no transcluded content is provided. * - * <example module="transcludeFallbackContentExample"> + * <example module="transcludeFallbackContentExample" name="ng-transclude"> * <file name="index.html"> * <script> * angular.module('transcludeFallbackContentExample', []) @@ -30618,7 +31989,7 @@ var ngSwitchDefaultDirective = ngDirective({ * </file> * <file name="app.js"> * angular.module('multiSlotTranscludeExample', []) - * .directive('pane', function(){ + * .directive('pane', function() { * return { * restrict: 'E', * transclude: { @@ -30635,7 +32006,7 @@ var ngSwitchDefaultDirective = ngDirective({ * }) * .controller('ExampleController', ['$scope', function($scope) { * $scope.title = 'Lorem Ipsum'; - * $scope.link = "https://google.com"; + * $scope.link = 'https://google.com'; * $scope.text = 'Neque porro quisquam est qui dolorem ipsum quia dolor...'; * }]); * </file> @@ -30691,7 +32062,7 @@ var ngTranscludeDirective = ['$compile', function($compile) { } function ngTranscludeCloneAttachFn(clone, transcludedScope) { - if (clone.length) { + if (clone.length && notWhitespace(clone)) { $element.append(clone); } else { useFallbackContent(); @@ -30708,6 +32079,15 @@ var ngTranscludeDirective = ['$compile', function($compile) { $element.append(clone); }); } + + function notWhitespace(nodes) { + for (var i = 0, ii = nodes.length; i < ii; i++) { + var node = nodes[i]; + if (node.nodeType !== NODE_TYPE_TEXT || node.nodeValue.trim()) { + return true; + } + } + } }; } }; @@ -30729,7 +32109,7 @@ var ngTranscludeDirective = ['$compile', function($compile) { * @param {string} id Cache name of the template. * * @example - <example> + <example name="script-tag"> <file name="index.html"> <script type="text/ng-template" id="/tpl.html"> Content of the template. @@ -30751,7 +32131,7 @@ var scriptDirective = ['$templateCache', function($templateCache) { restrict: 'E', terminal: true, compile: function(element, attr) { - if (attr.type == 'text/ng-template') { + if (attr.type === 'text/ng-template') { var templateUrl = attr.id, text = element[0].text; @@ -30761,15 +32141,20 @@ var scriptDirective = ['$templateCache', function($templateCache) { }; }]; +/* 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 <option selected="selected"> element to a <select required="required"> should - // automatically select the new element - if (optionElement[0].hasAttribute('selected')) { - optionElement[0].selected = true; - } +function setOptionSelectedStatus(optionEl, value) { + optionEl.prop('selected', value); // needed for IE + /** + * When unselecting an option, setting the property to null / false should be enough + * However, screenreaders might react to the selected attribute instead, see + * https://github.com/angular/angular.js/issues/14419 + * Note: "selected" is a boolean attr and will be removed when the "value" arg in attr() is false + * or null + */ + optionEl.attr('selected', value); } /** @@ -30781,13 +32166,16 @@ function chromeHack(optionElement) { * added `<option>` elements, perhaps by an `ngRepeat` directive. */ var SelectController = - ['$element', '$scope', function($element, $scope) { + ['$element', '$scope', /** @this */ function($element, $scope) { var self = this, - optionsMap = new HashMap(); + optionsMap = new NgMap(); + + self.selectValueMap = {}; // Keys are the hashed values, values the original values // If the ngModel doesn't get provided then provide a dummy noop version to prevent errors self.ngModelCtrl = noopNgModelController; + self.multiple = false; // The "unknown" option is one that is prepended to the list if the viewValue // does not match any of the options. When it is rendered the value of the unknown @@ -30796,42 +32184,94 @@ var SelectController = // We can't just jqLite('<option>') since jqLite is not smart enough // to create it in <select> and IE barfs otherwise. self.unknownOption = jqLite(window.document.createElement('option')); + + // The empty option is an option with the value '' that te application developer can + // provide inside the select. When the model changes to a value that doesn't match an option, + // it is selected - so if an empty option is provided, no unknown option is generated. + // However, the empty option is not removed when the model matches an option. It is always selectable + // and indicates that a "null" selection has been made. + self.hasEmptyOption = false; + self.emptyOption = undefined; + self.renderUnknownOption = function(val) { - var unknownVal = '? ' + hashKey(val) + ' ?'; + var unknownVal = self.generateUnknownOptionValue(val); self.unknownOption.val(unknownVal); $element.prepend(self.unknownOption); + setOptionSelectedStatus(self.unknownOption, true); $element.val(unknownVal); }; - $scope.$on('$destroy', function() { - // disable unknown option so that we don't do work when the whole select is being destroyed - self.renderUnknownOption = noop; - }); + self.updateUnknownOption = function(val) { + var unknownVal = self.generateUnknownOptionValue(val); + self.unknownOption.val(unknownVal); + setOptionSelectedStatus(self.unknownOption, true); + $element.val(unknownVal); + }; + + self.generateUnknownOptionValue = function(val) { + return '? ' + hashKey(val) + ' ?'; + }; self.removeUnknownOption = function() { if (self.unknownOption.parent()) self.unknownOption.remove(); }; + self.selectEmptyOption = function() { + if (self.emptyOption) { + $element.val(''); + setOptionSelectedStatus(self.emptyOption, true); + } + }; + + self.unselectEmptyOption = function() { + if (self.hasEmptyOption) { + self.emptyOption.removeAttr('selected'); + } + }; + + $scope.$on('$destroy', function() { + // disable unknown option so that we don't do work when the whole select is being destroyed + self.renderUnknownOption = noop; + }); // Read the value of the select control, the implementation of this changes depending // upon whether the select can have multiple values and whether ngOptions is at work. self.readValue = function readSingleValue() { - self.removeUnknownOption(); - return $element.val(); + var val = $element.val(); + // ngValue added option values are stored in the selectValueMap, normal interpolations are not + var realVal = val in self.selectValueMap ? self.selectValueMap[val] : val; + + if (self.hasOption(realVal)) { + return realVal; + } + + return null; }; // Write the value to the select control, the implementation of this changes depending // upon whether the select can have multiple values and whether ngOptions is at work. self.writeValue = function writeSingleValue(value) { + // Make sure to remove the selected attribute from the previously selected option + // Otherwise, screen readers might get confused + var currentlySelectedOption = $element[0].options[$element[0].selectedIndex]; + if (currentlySelectedOption) setOptionSelectedStatus(jqLite(currentlySelectedOption), false); + if (self.hasOption(value)) { self.removeUnknownOption(); - $element.val(value); - if (value === '') self.emptyOption.prop('selected', true); // to make IE9 happy + + var hashedVal = hashKey(value); + $element.val(hashedVal in self.selectValueMap ? hashedVal : value); + + // Set selected attribute and property on selected option for screen readers + var selectedOption = $element[0].options[$element[0].selectedIndex]; + setOptionSelectedStatus(jqLite(selectedOption), true); } else { if (value == null && self.emptyOption) { self.removeUnknownOption(); - $element.val(''); + self.selectEmptyOption(); + } else if (self.unknownOption.parent().length) { + self.updateUnknownOption(value); } else { self.renderUnknownOption(value); } @@ -30846,12 +32286,14 @@ var SelectController = assertNotHasOwnProperty(value, '"option value"'); if (value === '') { + self.hasEmptyOption = true; self.emptyOption = element; } var count = optionsMap.get(value) || 0; - optionsMap.put(value, count + 1); - self.ngModelCtrl.$render(); - chromeHack(element); + optionsMap.set(value, count + 1); + // Only render at the end of a digest. This improves render performance when many options + // are added during a digest and ensures all relevant options are correctly marked as selected + scheduleRender(); }; // Tell the select control that an option, with the given value, has been removed @@ -30859,12 +32301,13 @@ var SelectController = var count = optionsMap.get(value); if (count) { if (count === 1) { - optionsMap.remove(value); + optionsMap.delete(value); if (value === '') { + self.hasEmptyOption = false; self.emptyOption = undefined; } } else { - optionsMap.put(value, count - 1); + optionsMap.set(value, count - 1); } } }; @@ -30875,35 +32318,131 @@ var SelectController = }; + var renderScheduled = false; + function scheduleRender() { + if (renderScheduled) return; + renderScheduled = true; + $scope.$$postDigest(function() { + renderScheduled = false; + self.ngModelCtrl.$render(); + }); + } + + var updateScheduled = false; + function scheduleViewValueUpdate(renderAfter) { + if (updateScheduled) return; + + updateScheduled = true; + + $scope.$$postDigest(function() { + if ($scope.$$destroyed) return; + + updateScheduled = false; + self.ngModelCtrl.$setViewValue(self.readValue()); + if (renderAfter) self.ngModelCtrl.$render(); + }); + } + + self.registerOption = function(optionScope, optionElement, optionAttrs, interpolateValueFn, interpolateTextFn) { - if (interpolateValueFn) { + if (optionAttrs.$attr.ngValue) { + // The value attribute is set by ngValue + var oldVal, hashedVal = NaN; + optionAttrs.$observe('value', function valueAttributeObserveAction(newVal) { + + var removal; + var previouslySelected = optionElement.prop('selected'); + + if (isDefined(hashedVal)) { + self.removeOption(oldVal); + delete self.selectValueMap[hashedVal]; + removal = true; + } + + hashedVal = hashKey(newVal); + oldVal = newVal; + self.selectValueMap[hashedVal] = newVal; + self.addOption(newVal, optionElement); + // Set the attribute directly instead of using optionAttrs.$set - this stops the observer + // from firing a second time. Other $observers on value will also get the result of the + // ngValue expression, not the hashed value + optionElement.attr('value', hashedVal); + + if (removal && previouslySelected) { + scheduleViewValueUpdate(); + } + + }); + } else if (interpolateValueFn) { // The value attribute is interpolated - var oldVal; optionAttrs.$observe('value', function valueAttributeObserveAction(newVal) { + // This method is overwritten in ngOptions and has side-effects! + self.readValue(); + + var removal; + var previouslySelected = optionElement.prop('selected'); + if (isDefined(oldVal)) { self.removeOption(oldVal); + removal = true; } oldVal = newVal; self.addOption(newVal, optionElement); + + if (removal && previouslySelected) { + scheduleViewValueUpdate(); + } }); } else if (interpolateTextFn) { // The text content is interpolated optionScope.$watch(interpolateTextFn, function interpolateWatchAction(newVal, oldVal) { optionAttrs.$set('value', newVal); + var previouslySelected = optionElement.prop('selected'); if (oldVal !== newVal) { self.removeOption(oldVal); } self.addOption(newVal, optionElement); + + if (oldVal && previouslySelected) { + scheduleViewValueUpdate(); + } }); } else { // The value attribute is static self.addOption(optionAttrs.value, optionElement); } + + optionAttrs.$observe('disabled', function(newVal) { + + // Since model updates will also select disabled options (like ngOptions), + // we only have to handle options becoming disabled, not enabled + + if (newVal === 'true' || newVal && optionElement.prop('selected')) { + if (self.multiple) { + scheduleViewValueUpdate(true); + } else { + self.ngModelCtrl.$setViewValue(null); + self.ngModelCtrl.$render(); + } + } + }); + optionElement.on('$destroy', function() { - self.removeOption(optionAttrs.value); - self.ngModelCtrl.$render(); + var currentValue = self.readValue(); + var removeValue = optionAttrs.value; + + self.removeOption(removeValue); + scheduleRender(); + + if (self.multiple && currentValue && currentValue.indexOf(removeValue) !== -1 || + currentValue === removeValue + ) { + // When multiple (selected) options are destroyed at the same time, we don't want + // to run a model update for each of them. Instead, run a single update in the $$postDigest + scheduleViewValueUpdate(true); + } }); }; }]; @@ -30914,7 +32453,7 @@ var SelectController = * @restrict E * * @description - * HTML `SELECT` element with angular data-binding. + * HTML `select` element with angular data-binding. * * The `select` directive is used together with {@link ngModel `ngModel`} to provide data-binding * between the scope and the `<select>` control (including setting default values). @@ -30924,14 +32463,24 @@ var SelectController = * When an item in the `<select>` menu is selected, the value of the selected option will be bound * to the model identified by the `ngModel` directive. With static or repeated options, this is * the content of the `value` attribute or the textContent of the `<option>`, if the value attribute is missing. - * If you want dynamic value attributes, you can use interpolation inside the value attribute. + * Value and textContent can be interpolated. * - * <div class="alert alert-warning"> - * Note that the value of a `select` directive used without `ngOptions` is always a string. - * When the model needs to be bound to a non-string value, you must either explicitly convert it - * using a directive (see example below) or use `ngOptions` to specify the set of options. - * This is because an option element can only be bound to string values at present. - * </div> + * ## Matching model and option values + * + * In general, the match between the model and an option is evaluated by strictly comparing the model + * value against the value of the available options. + * + * If you are setting the option value with the option's `value` attribute, or textContent, the + * value will always be a `string` which means that the model value must also be a string. + * Otherwise the `select` directive cannot match them correctly. + * + * To bind the model to a non-string value, you can use one of the following strategies: + * - the {@link ng.ngOptions `ngOptions`} directive + * ({@link ng.select#using-select-with-ngoptions-and-setting-a-default-value}) + * - the {@link ng.ngValue `ngValue`} directive, which allows arbitrary expressions to be + * option values ({@link ng.select#using-ngvalue-to-bind-the-model-to-an-array-of-objects Example}) + * - model $parsers / $formatters to convert the string value + * ({@link ng.select#binding-select-to-a-non-string-value-via-ngmodel-parsing-formatting Example}) * * If the viewValue of `ngModel` does not match any of the options, then the control * will automatically add an "unknown" option, which it then removes when the mismatch is resolved. @@ -30940,13 +32489,17 @@ var SelectController = * be nested into the `<select>` element. This element will then represent the `null` or "not selected" * option. See example below for demonstration. * - * <div class="alert alert-info"> + * ## Choosing between `ngRepeat` and `ngOptions` + * * In many cases, `ngRepeat` can be used on `<option>` elements instead of {@link ng.directive:ngOptions - * ngOptions} to achieve a similar result. However, `ngOptions` provides some benefits, such as - * more flexibility in how the `<select>`'s model is assigned via the `select` **`as`** part of the - * comprehension expression, and additionally in reducing memory and increasing speed by not creating - * a new scope for each repeated instance. - * </div> + * ngOptions} to achieve a similar result. However, `ngOptions` provides some benefits: + * - more flexibility in how the `<select>`'s model is assigned via the `select` **`as`** part of the + * comprehension expression + * - reduced memory consumption by not creating a new scope for each repeated instance + * - increased render speed by creating the options in a documentFragment instead of individually + * + * Specifically, select with repeated options slows down significantly starting at 2000 options in + * Chrome and Internet Explorer / Edge. * * * @param {string} ngModel Assignable angular expression to data-bind to. @@ -30961,6 +32514,8 @@ var SelectController = * interaction with the select element. * @param {string=} ngOptions sets the options that the select is populated with and defines what is * set on the model on selection. See {@link ngOptions `ngOptions`}. + * @param {string=} ngAttrSize sets the size of the select element dynamically. Uses the + * {@link guide/interpolation#-ngattr-for-binding-to-arbitrary-attributes ngAttr} directive. * * @example * ### Simple `select` elements with static options @@ -31001,7 +32556,7 @@ var SelectController = * $scope.data = { * singleSelect: null, * multipleSelect: [], - * option1: 'option-1', + * option1: 'option-1' * }; * * $scope.forceUnknownOption = function() { @@ -31012,34 +32567,65 @@ var SelectController = *</example> * * ### Using `ngRepeat` to generate `select` options - * <example name="ngrepeat-select" module="ngrepeatSelect"> + * <example name="select-ngrepeat" module="ngrepeatSelect"> * <file name="index.html"> * <div ng-controller="ExampleController"> * <form name="myForm"> * <label for="repeatSelect"> Repeat select: </label> - * <select name="repeatSelect" id="repeatSelect" ng-model="data.repeatSelect"> + * <select name="repeatSelect" id="repeatSelect" ng-model="data.model"> * <option ng-repeat="option in data.availableOptions" value="{{option.id}}">{{option.name}}</option> * </select> * </form> * <hr> - * <tt>repeatSelect = {{data.repeatSelect}}</tt><br/> + * <tt>model = {{data.model}}</tt><br/> * </div> * </file> * <file name="app.js"> * angular.module('ngrepeatSelect', []) * .controller('ExampleController', ['$scope', function($scope) { * $scope.data = { - * repeatSelect: null, + * model: null, * availableOptions: [ * {id: '1', name: 'Option A'}, * {id: '2', name: 'Option B'}, * {id: '3', name: 'Option C'} - * ], + * ] * }; * }]); * </file> *</example> * + * ### Using `ngValue` to bind the model to an array of objects + * <example name="select-ngvalue" module="ngvalueSelect"> + * <file name="index.html"> + * <div ng-controller="ExampleController"> + * <form name="myForm"> + * <label for="ngvalueselect"> ngvalue select: </label> + * <select size="6" name="ngvalueselect" ng-model="data.model" multiple> + * <option ng-repeat="option in data.availableOptions" ng-value="option.value">{{option.name}}</option> + * </select> + * </form> + * <hr> + * <pre>model = {{data.model | json}}</pre><br/> + * </div> + * </file> + * <file name="app.js"> + * angular.module('ngvalueSelect', []) + * .controller('ExampleController', ['$scope', function($scope) { + * $scope.data = { + * model: null, + * availableOptions: [ + {value: 'myString', name: 'string'}, + {value: 1, name: 'integer'}, + {value: true, name: 'boolean'}, + {value: null, name: 'null'}, + {value: {prop: 'value'}, name: 'object'}, + {value: ['a'], name: 'array'} + * ] + * }; + * }]); + * </file> + *</example> * * ### Using `select` with `ngOptions` and setting a default value * See the {@link ngOptions ngOptions documentation} for more `ngOptions` usage examples. @@ -31105,7 +32691,6 @@ var SelectController = * </file> * <file name="protractor.js" type="protractor"> * it('should initialize to model', function() { - * var select = element(by.css('select')); * expect(element(by.model('model.id')).$('option:checked').getText()).toEqual('Two'); * }); * </file> @@ -31127,11 +32712,16 @@ var selectDirective = function() { function selectPreLink(scope, element, attr, ctrls) { - // if ngModel is not defined, we don't need to do anything + var selectCtrl = ctrls[0]; var ngModelCtrl = ctrls[1]; - if (!ngModelCtrl) return; - var selectCtrl = ctrls[0]; + // if ngModel is not defined, we don't need to do anything but set the registerOption + // function to noop, so options don't get added internally + if (!ngModelCtrl) { + selectCtrl.registerOption = noop; + return; + } + selectCtrl.ngModelCtrl = ngModelCtrl; @@ -31139,6 +32729,7 @@ var selectDirective = function() { // to the `readValue` method, which can be changed if the select can have multiple // selected values or if the options are being generated by `ngOptions` element.on('change', function() { + selectCtrl.removeUnknownOption(); scope.$apply(function() { ngModelCtrl.$setViewValue(selectCtrl.readValue()); }); @@ -31149,13 +32740,15 @@ var selectDirective = function() { // we have to add an extra watch since ngModel doesn't work well with arrays - it // doesn't trigger rendering if only an item in the array changes. if (attr.multiple) { + selectCtrl.multiple = true; // Read value now needs to check each option to see if it is selected selectCtrl.readValue = function readMultipleValue() { var array = []; forEach(element.find('option'), function(option) { - if (option.selected) { - array.push(option.value); + if (option.selected && !option.disabled) { + var val = option.value; + array.push(val in selectCtrl.selectValueMap ? selectCtrl.selectValueMap[val] : val); } }); return array; @@ -31163,9 +32756,21 @@ var selectDirective = function() { // Write value now needs to set the selected property of each matching option selectCtrl.writeValue = function writeMultipleValue(value) { - var items = new HashMap(value); forEach(element.find('option'), function(option) { - option.selected = isDefined(items.get(option.value)); + var shouldBeSelected = !!value && (includes(value, option.value) || + includes(value, selectCtrl.selectValueMap[option.value])); + var currentlySelected = option.selected; + + // IE and Edge, adding options to the selection via shift+click/UP/DOWN, + // will de-select already selected options if "selected" on those options was set + // more than once (i.e. when the options were already selected) + // So we only modify the selected property if neccessary. + // Note: this behavior cannot be replicated via unit tests because it only shows in the + // actual user interface. + if (shouldBeSelected !== currentlySelected) { + setOptionSelectedStatus(jqLite(option), shouldBeSelected); + } + }); }; @@ -31216,13 +32821,17 @@ var optionDirective = ['$interpolate', function($interpolate) { restrict: 'E', priority: 100, compile: function(element, attr) { - if (isDefined(attr.value)) { + var interpolateValueFn, interpolateTextFn; + + if (isDefined(attr.ngValue)) { + // Will be handled by registerOption + } else if (isDefined(attr.value)) { // If the value attribute is defined, check if it contains an interpolation - var interpolateValueFn = $interpolate(attr.value, true); + interpolateValueFn = $interpolate(attr.value, true); } else { // If the value attribute is not defined then we fall back to the // text content of the option element, which may be interpolated - var interpolateTextFn = $interpolate(element.text(), true); + interpolateTextFn = $interpolate(element.text(), true); if (!interpolateTextFn) { attr.$set('value', element.text()); } @@ -31244,11 +32853,6 @@ var optionDirective = ['$interpolate', function($interpolate) { }; }]; -var styleDirective = valueFn({ - restrict: 'E', - terminal: false -}); - /** * @ngdoc directive * @name ngRequired @@ -31509,7 +33113,7 @@ var maxlengthDirective = function() { var maxlength = -1; attr.$observe('maxlength', function(value) { var intVal = toInt(value); - maxlength = isNaN(intVal) ? -1 : intVal; + maxlength = isNumberNaN(intVal) ? -1 : intVal; ctrl.$validate(); }); ctrl.$validators.maxlength = function(modelValue, viewValue) { @@ -31603,15 +33207,15 @@ var minlengthDirective = function() { }; if (window.angular.bootstrap) { - //AngularJS is already loaded, so we can return here... + // AngularJS is already loaded, so we can return here... if (window.console) { console.log('WARNING: Tried to load angular more than once.'); } return; } -//try to bind to jquery now so that one can write jqLite(document).ready() -//but we will rebind on bootstrap again. +// try to bind to jquery now so that one can write jqLite(fn) +// but we will rebind on bootstrap again. bindJQuery(); publishExternalAPI(angular); @@ -31759,7 +33363,7 @@ $provide.value("$locale", { }); }]); - jqLite(window.document).ready(function() { + jqLite(function() { angularInit(window.document, bootstrap); }); diff --git a/static/js/bmcApp.js b/static/js/bmcApp.js index 7575e346ce..55e4a425e2 100644 --- a/static/js/bmcApp.js +++ b/static/js/bmcApp.js @@ -1,88 +1,79 @@ 'use strict'; angular.module('Authentication', []); var app = angular.module('bmcApp', [ - 'Authentication', - 'ngCookies', - 'ui.bootstrap', - 'ui.router', - 'ngSanitize', - 'ngWebSocket', - 'ngResource' + 'ngCookies', 'ngAnimate', 'ngSanitize', 'ui.bootstrap', + 'ui.router', 'ngWebSocket', 'Authentication', 'ui.router.modal', ]); - - -app.controller('MainCtrl', ['$scope', function($scope) { - -}]); - -app.service('loginInterceptor', ["$injector", +app.service('loginInterceptor', [ + '$injector', function($injector) { var service = this; service.responseError = function(response) { - var $state = $injector.get('$state'); - var AuthenticationService = $injector.get('AuthenticationService'); - if (response.status == 401){ - console.log("Login required... "); - - var invalidate_reason = "Your user was logged out."; - - // if we're attempting to log in, we need to - // continue the promise chain to make sure the user is informed - if ($state.current.name === "login") { - invalidate_reason = "Your username and password was incorrect"; - } else { - $state.after_login_state = $state.current.name; - $state.go('login'); - } - AuthenticationService.ClearCredentials(invalidate_reason); + var $state = $injector.get('$state'); + var AuthenticationService = $injector.get('AuthenticationService'); + if (response.status == 401) { + console.log('Login required... '); + + var invalidate_reason = 'Your user was logged out.'; + + // if we're attempting to log in, we need to + // continue the promise chain to make sure the user is informed + if ($state.current.name === 'login') { + invalidate_reason = 'Your username and password was incorrect'; + } else { + $state.after_login_state = $state.current.name; + $state.go('login'); + } + AuthenticationService.ClearCredentials(invalidate_reason); } - return response; + return response; }; -}]) + } +]) -app.config(['$httpProvider', function ($httpProvider) { +app.config([ + '$httpProvider', + function($httpProvider) { $httpProvider.interceptors.push('loginInterceptor'); -}]); - -app.directive('windowSize', ['$window', function ($window) { - return function (scope, element) { - var w = angular.element($window); - scope.getWindowDimensions = function () { - return { - 'h': w.height(), - 'w': w.width() - }; - }; - scope.$watch(scope.getWindowDimensions, function (newValue, oldValue) { - scope.windowHeight = newValue.h; - scope.windowWidth = newValue.w; - scope.style = function () { + } +]); + +app.directive('windowSize', [ + '$window', + function($window) { + return function(scope, element) { + var w = angular.element($window); + scope.getWindowDimensions = function() { + return {'h' : w.height(), 'w' : w.width()}; + }; + scope.$watch(scope.getWindowDimensions, function(newValue, oldValue) { + scope.windowHeight = newValue.h; + scope.windowWidth = newValue.w; + scope.style = function() { return { - 'height': (newValue.h - 100) + 'px', - 'width': (newValue.w - 100) + 'px' + 'height' : (newValue.h - 100) + 'px', + 'width' : (newValue.w - 100) + 'px' }; - }; - }, true); + }; + }, true); - w.bind('resize', function () { - scope.$apply(); - }); + w.bind('resize', function() { scope.$apply(); }); + } } -}]); - -app.run(['$rootScope', '$cookieStore', '$state', '$resource', 'AuthenticationService', '$http', '$templateCache', - function($rootScope, $cookieStore, $state, $resource, AuthenticationService, $http, $templateCache) { +]); - $http.get('static/partial-login.html', {cache:$templateCache}); - $http.get('static/partial-kvm.html', {cache: $templateCache}); +app.run([ + '$rootScope', '$cookieStore', '$state', 'AuthenticationService', '$http', + '$templateCache', + function($rootScope, $cookieStore, $state, AuthenticationService, $http, + $templateCache) { - if ($rootScope.globals == undefined){ - $rootScope.globals = {}; + if ($rootScope.globals == undefined) { + $rootScope.globals = {}; } - // keep user logged in after page refresh AuthenticationService.RestoreCredientials(); @@ -90,7 +81,8 @@ app.run(['$rootScope', '$cookieStore', '$state', '$resource', 'AuthenticationSer '$stateChangeStart', function(event, toState, toParams, fromState, fromParams, options) { // redirect to login page if not logged in - // unless we're already trying to go to the login page (prevent a loop) + // unless we're already trying to go to the login page (prevent a + // loop) if (!$rootScope.globals.currentUser && toState.name !== 'login') { // If logged out and transitioning to a logged in page: event.preventDefault(); @@ -101,108 +93,137 @@ app.run(['$rootScope', '$cookieStore', '$state', '$resource', 'AuthenticationSer } ]); -app.config(['$stateProvider', '$urlRouterProvider', - function($stateProvider, $urlRouterProvider) { +app.config([ + '$stateProvider', '$urlRouterProvider', + function($stateProvider, $urlRouterProvider) { + + $urlRouterProvider.otherwise('/systeminfo'); + + $stateProvider + .state('login', { + url : '/login', + templateUrl : 'static/partial-login.html', + controller : 'LoginController', + }) + .state('systeminfo', { + url : '/systeminfo', + templateUrl : 'static/partial-systeminfo.html' + }) + .state( + 'eventlog', + {url : '/eventlog', templateUrl : 'static/partial-eventlog.html'}) + + .state('kvm', {url : '/kvm', templateUrl : 'static/partial-kvm.html'}) + + .state('ipmi', + {url : '/ipmi', templateUrl : 'static/partial-ipmi.html'}) + + .state('sensor', + {url : '/sensor', templateUrl : 'static/partial-sensor.html'}) + + .state( + 'fwupdate', + {url : '/fwupdate', templateUrl : 'static/partial-fwupdate.html'}) + // nested list with custom controller + .state('fwupdate.confirm', { + url : '/confirm', + templateUrl : 'static/partial-fwupdateconfirm.html', + modal: true + }) + // ABOUT PAGE AND MULTIPLE NAMED VIEWS ================================= + .state('about', + {url : '/about', templateUrl : 'static/partial-fruinfo.html'}) + + // nested list with custom controller + .state('about.list', { + url : '/list', + templateUrl : 'static/partial-home-list.html', + controller : function($scope) { + $scope.dogs = [ 'Bernese', 'Husky', 'Goldendoodle' ]; + } + }); - $urlRouterProvider.otherwise('/systeminfo'); + } +]); - $stateProvider - // nested list with just some random string data - .state('login', { - url: '/login', - templateUrl: 'static/partial-login.html', - controller: 'LoginController', - }) - // systeminfo view ======================================== - .state( - 'systeminfo', - {url: '/systeminfo', templateUrl: 'static/partial-systeminfo.html'}) +app.directive('fileread', [ function() { + return { + scope: {fileread : '='}, + link: function(scope, element, attributes) { + element.bind('change', function(changeEvent) { + scope.$apply(function() { + scope.fileread = changeEvent.target.files[0]; + // or all selected files: + // scope.fileread = changeEvent.target.files; + }); + }); + } + } + } ]); + +app.controller('PaginationDemoCtrl', [ + '$scope', '$log', + function($scope, $log) { + $scope.totalItems = 64; + $scope.currentPage = 4; + + $scope.setPage = function(pageNo) { $scope.currentPage = pageNo; }; + + $scope.pageChanged = function() { + $log.log('Page changed to: ' + $scope.currentPage); + }; + $scope.maxSize = 5; + $scope.bigTotalItems = 175; + $scope.bigCurrentPage = 1; + } +]); - // HOME STATES AND NESTED VIEWS ======================================== - .state( - 'eventlog', {url: '/eventlog', templateUrl: 'static/partial-eventlog.html'}) +angular.module('Authentication').factory('AuthenticationService', [ + '$cookieStore', '$rootScope', '$timeout', '$log', '$http', + function($cookieStore, $rootScope, $timeout, $log, $http) { + var service = {}; + service.Login = function(username, password) { + var user = {'username' : username, 'password' : password}; + return $http.post('/login', user); + }; - .state( - 'kvm', {url: '/kvm', templateUrl: 'static/partial-kvm.html'}) + service.SetCredentials = function(username, token) { + $rootScope.globals['currentUser'] = { + username : username, + authdata : token + }; + $http.defaults.headers.common['Authorization'] = 'Token ' + token; + $cookieStore.put('globals', $rootScope.globals); + }; - .state( - 'ipmi', {url: '/ipmi', templateUrl: 'static/partial-ipmi.html'}) + service.ClearCredentials = function(reason) { + $rootScope.globals['currentUser'] = null; + if (reason !== null) { + service.logoutreason = reason; + } + $cookieStore.remove('globals'); + $http.defaults.headers.common['Authorization'] = ''; + }; - .state( - 'sensor', {url: '/sensor', templateUrl: 'static/partial-sensor.html'}) + service.RestoreCredientials = function() { + var globals = $cookieStore.get('globals') || {}; + if (globals.currentUser) { + service.SetCredentials(globals.currentUser.username, + globals.currentUser.authdata); + } + }; - // ABOUT PAGE AND MULTIPLE NAMED VIEWS ================================= - .state('about', {url: '/about', templateUrl: 'static/partial-fruinfo.html'}) + service.IsLoggedIn = function() { + if ($rootScope.globals['currentUser']){ + return true; + } else { + return false; + } + }; - // nested list with custom controller - .state('about.list', { - url: '/list', - templateUrl: 'static/partial-home-list.html', - controller: function($scope) { - $scope.dogs = ['Bernese', 'Husky', 'Goldendoodle']; - } - }) - - -}]); - -app.controller('PaginationDemoCtrl', ['$scope', '$log', function($scope, $log) { - $scope.totalItems = 64; - $scope.currentPage = 4; - - $scope.setPage = function(pageNo) { $scope.currentPage = pageNo; }; - - $scope.pageChanged = function() { - $log.log('Page changed to: ' + $scope.currentPage); - }; - - $scope.maxSize = 5; - $scope.bigTotalItems = 175; - $scope.bigCurrentPage = 1; -}]); - -angular.module('Authentication').factory( - 'AuthenticationService', - ['$cookieStore', '$rootScope', '$timeout', '$resource', '$log', '$http', - function($cookieStore, $rootScope, $timeout, $resource, $log, $http) { - var service = {}; - - service.Login = function(username, password, success_callback, fail_callback) { - - var user = {"username": username, "password": password}; - var UserLogin = $resource("/login"); - var this_login = new UserLogin(); - this_login.data = {"username": username, "password": password}; - UserLogin.save(user, success_callback, fail_callback); - - }; - - service.SetCredentials = function(username, token) { - $rootScope.globals["currentUser"] = {username: username, authdata: token}; - $http.defaults.headers.common['Authorization'] = 'Token ' + token; - $cookieStore.put('globals', $rootScope.globals); - }; - - service.ClearCredentials = function(reason) { - $rootScope.globals["currentUser"] = null; - if (reason !== null) { - service.logoutreason = reason; - } - $cookieStore.remove('globals'); - $http.defaults.headers.common['Authorization'] = ''; - }; - - service.RestoreCredientials = function() { - var globals = $cookieStore.get('globals') || {}; - if (globals.currentUser) { - service.SetCredentials( - globals.currentUser.username, globals.currentUser.authdata); - } - }; - - service.logoutreason = ""; - return service; - } - ]); + service.logoutreason = ''; + return service; + } +]); diff --git a/static/js/fwupdateController.js b/static/js/fwupdateController.js new file mode 100644 index 0000000000..ceacd8a43c --- /dev/null +++ b/static/js/fwupdateController.js @@ -0,0 +1,83 @@ +angular.module('bmcApp').controller('fwupdateController', [ + '$scope', '$http', '$uibModal', + function($scope, $http, $uibModal, $state) { + $scope.upload = function(files) { + r = new FileReader(); + r.onload = function(e) { + get_image_info = function(buffer) { + image_info = {'valid' : false} + var expected = '*SignedImage*\0\0\0' + + var dv1 = new Int8Array(e.target.result, 0, 16); + + for (var i = 0; i != expected.length; i++) { + if (dv1[i] != expected.charCodeAt(i)) { + return image_info; + } + } + image_info['valid'] = true; + var generation = new Int8Array(e.target.result, 16, 17)[0]; + image_info['generation'] = generation; + if ((generation < 4) || + (generation > 5)) { // not VLN generation header + + return image_info; + } else { + var version_minor = new Uint16Array(e.target.result, 20, 22)[0]; + image_info['major_version'] = + new Uint8Array(e.target.result, 28, 29)[0]; + image_info['submajor_version'] = + new Uint8Array(e.target.result, 29, 30)[0].toString(16); + var version_minor2 = new Uint16Array(e.target.result, 30, 32)[0]; + image_info['sha1_version'] = + ('0000' + version_minor2.toString(16)).substr(-4) + + ('0000' + version_minor.toString(16)).substr(-4); + } + return image_info; + }; + var image_info = get_image_info(e.target.result); + $scope.image_info = image_info; + + var objectSelectionModal = $uibModal.open({ + templateUrl : 'static/partial-fwupdateconfirm.html', + controller : function($scope) { + $scope.image_info = image_info; + $scope.file_to_load = file_to_load; + // The function that is called for modal closing (positive button) + + $scope.okModal = function() { + // Closing the model with result + objectSelectionModal.close($scope.selection); + $http + .post( + '/intel/firmwareupload', data = e.target.result, + {headers : {'Content-Type' : 'application/octet-stream'}}) + .then( + function successCallback(response) { + console.log('Success uploaded. Response: ' + + response.data) + }, + function errorCallback(response) { + console.log('Error status: ' + response.status) + }); + }; + + // The function that is called for modal dismissal(negative button) + + $scope.dismissModal = function() { + objectSelectionModal.dismiss(); + }; + + } + + }); + }; + var file_to_load = files[0]; + $scope.file_to_load = file_to_load; + r.readAsArrayBuffer(files[0]); + + }; + + $scope.filename = ''; + } +]);
\ No newline at end of file diff --git a/static/js/fwupdateconfirmController.js b/static/js/fwupdateconfirmController.js new file mode 100644 index 0000000000..2da1cb2bee --- /dev/null +++ b/static/js/fwupdateconfirmController.js @@ -0,0 +1,5 @@ +angular.module('bmcApp').controller('fwupdateconfirmController', [ + '$scope', '$stateParams',{ + $scope.filename = $stateParams.filename; + } +]);
\ No newline at end of file diff --git a/static/js/loginController.js b/static/js/loginController.js index e83d8bc686..f2168baba4 100644 --- a/static/js/loginController.js +++ b/static/js/loginController.js @@ -1,32 +1,39 @@ angular.module('Authentication').controller('LoginController', [ - '$scope', '$rootScope', '$location', '$state', 'AuthenticationService', - function($scope, $rootScope, $location, $state, AuthenticationService) { + '$scope', '$rootScope', '$location', '$state', 'AuthenticationService', + function($scope, $rootScope, $location, $state, AuthenticationService) { + if (AuthenticationService.IsLoggedIn()) { + if (typeof $state.after_login_state === 'undefined') { + $state.after_login_state = 'systeminfo'; + } + $state.go($state.after_login_state); + delete $state.after_login_state; + } $scope.logoutreason = AuthenticationService.logoutreason; $scope.login = function() { - $scope.dataLoading = true; - AuthenticationService.Login( - $scope.username, $scope.password, - function(response) { - AuthenticationService.SetCredentials( - $scope.username, response.token); - if (typeof $state.after_login_state === "undefined") { - $state.after_login_state = "systeminfo"; - } - $state.go($state.after_login_state); - delete $state.after_login_state; + $scope.dataLoading = true; + AuthenticationService.Login($scope.username, $scope.password) + .then( + function(response) { + AuthenticationService.SetCredentials($scope.username, + response.data.token); + if (typeof $state.after_login_state === 'undefined') { + $state.after_login_state = 'systeminfo'; + } + $state.go($state.after_login_state); + delete $state.after_login_state; - }, - function(response) { - if (response.status === 401) { - // reset login status - AuthenticationService.ClearCredentials( - "Username or Password is incorrect"); - } - $scope.logoutreason = AuthenticationService.logoutreason; - $scope.error = response.message; - $scope.dataLoading = false; - }); + }, + function(response) { + if (response.status === 401) { + // reset login status + AuthenticationService.ClearCredentials( + 'Username or Password is incorrect'); + } + $scope.logoutreason = AuthenticationService.logoutreason; + $scope.error = response.message; + $scope.dataLoading = false; + }); }; - } + } ]); diff --git a/static/js/mainController.js b/static/js/mainController.js new file mode 100644 index 0000000000..c0be7e7dc9 --- /dev/null +++ b/static/js/mainController.js @@ -0,0 +1,6 @@ +angular.module('bmcApp').controller('MainCtrl', [ + '$scope', 'AuthenticationService', + function($scope, AuthenticationService) { + $scope.is_logged_in = AuthenticationService.IsLoggedIn; + } +]);
\ No newline at end of file diff --git a/static/js/sensorController.js b/static/js/sensorController.js index 3554c0f507..f1e0812f4c 100644 --- a/static/js/sensorController.js +++ b/static/js/sensorController.js @@ -1,11 +1,8 @@ angular.module('bmcApp').controller('sensorController', [ - '$scope', '$resource', - function($scope, $resource) { - - var systeminfo = $resource("/sensortest"); - systeminfo.get(function(sensor_values) { + '$scope', '$http', + function($scope, $http) { + $http.get('/sensortest').then(function(sensor_values) { $scope.sensor_values = sensor_values; - }); - + }) } ]);
\ No newline at end of file diff --git a/static/js/ui-bootstrap-tpls-2.1.3.js b/static/js/ui-bootstrap-tpls-2.5.0.js index b742ed8ed8..6783893672 100644 --- a/static/js/ui-bootstrap-tpls-2.1.3.js +++ b/static/js/ui-bootstrap-tpls-2.5.0.js @@ -2,9 +2,9 @@ * angular-ui-bootstrap * http://angular-ui.github.io/bootstrap/ - * Version: 2.1.3 - 2016-08-25 + * Version: 2.5.0 - 2017-01-28 * License: MIT - */angular.module("ui.bootstrap", ["ui.bootstrap.tpls", "ui.bootstrap.collapse","ui.bootstrap.tabindex","ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.buttons","ui.bootstrap.carousel","ui.bootstrap.dateparser","ui.bootstrap.isClass","ui.bootstrap.datepicker","ui.bootstrap.position","ui.bootstrap.datepickerPopup","ui.bootstrap.debounce","ui.bootstrap.dropdown","ui.bootstrap.stackedMap","ui.bootstrap.modal","ui.bootstrap.paging","ui.bootstrap.pager","ui.bootstrap.pagination","ui.bootstrap.tooltip","ui.bootstrap.popover","ui.bootstrap.progressbar","ui.bootstrap.rating","ui.bootstrap.tabs","ui.bootstrap.timepicker","ui.bootstrap.typeahead"]); + */angular.module("ui.bootstrap", ["ui.bootstrap.tpls", "ui.bootstrap.collapse","ui.bootstrap.tabindex","ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.buttons","ui.bootstrap.carousel","ui.bootstrap.dateparser","ui.bootstrap.isClass","ui.bootstrap.datepicker","ui.bootstrap.position","ui.bootstrap.datepickerPopup","ui.bootstrap.debounce","ui.bootstrap.multiMap","ui.bootstrap.dropdown","ui.bootstrap.stackedMap","ui.bootstrap.modal","ui.bootstrap.paging","ui.bootstrap.pager","ui.bootstrap.pagination","ui.bootstrap.tooltip","ui.bootstrap.popover","ui.bootstrap.progressbar","ui.bootstrap.rating","ui.bootstrap.tabs","ui.bootstrap.timepicker","ui.bootstrap.typeahead"]); angular.module("ui.bootstrap.tpls", ["uib/template/accordion/accordion-group.html","uib/template/accordion/accordion.html","uib/template/alert/alert.html","uib/template/carousel/carousel.html","uib/template/carousel/slide.html","uib/template/datepicker/datepicker.html","uib/template/datepicker/day.html","uib/template/datepicker/month.html","uib/template/datepicker/year.html","uib/template/datepickerPopup/popup.html","uib/template/modal/window.html","uib/template/pager/pager.html","uib/template/pagination/pagination.html","uib/template/tooltip/tooltip-html-popup.html","uib/template/tooltip/tooltip-popup.html","uib/template/tooltip/tooltip-template-popup.html","uib/template/popover/popover-html.html","uib/template/popover/popover-template.html","uib/template/popover/popover.html","uib/template/progressbar/bar.html","uib/template/progressbar/progress.html","uib/template/progressbar/progressbar.html","uib/template/rating/rating.html","uib/template/tabs/tab.html","uib/template/tabs/tabset.html","uib/template/timepicker/timepicker.html","uib/template/typeahead/typeahead-match.html","uib/template/typeahead/typeahead-popup.html"]); angular.module('ui.bootstrap.collapse', []) @@ -80,7 +80,7 @@ angular.module('ui.bootstrap.collapse', []) to: getScrollFromElement(element[0]) }).then(expandDone); } - }); + }, angular.noop); } function expandDone() { @@ -119,7 +119,7 @@ angular.module('ui.bootstrap.collapse', []) to: cssTo }).then(collapseDone); } - }); + }, angular.noop); } function collapseDone() { @@ -436,7 +436,7 @@ angular.module('ui.bootstrap.carousel', []) slides = self.slides = $scope.slides = [], SLIDE_DIRECTION = 'uib-slideDirection', currentIndex = $scope.active, - currentInterval, isPlaying, bufferedTransitions = []; + currentInterval, isPlaying; var destroyed = false; $element.addClass('carousel'); @@ -498,11 +498,6 @@ angular.module('ui.bootstrap.carousel', []) self.removeSlide = function(slide) { var index = findSlideIndex(slide); - var bufferedIndex = bufferedTransitions.indexOf(slides[index]); - if (bufferedIndex !== -1) { - bufferedTransitions.splice(bufferedIndex, 1); - } - //get the index of the slide inside the carousel slides.splice(index, 1); if (slides.length > 0 && currentIndex === index) { @@ -526,7 +521,6 @@ angular.module('ui.bootstrap.carousel', []) if (slides.length === 0) { currentIndex = null; $scope.active = null; - clearBufferedTransitions(); } }; @@ -541,8 +535,6 @@ angular.module('ui.bootstrap.carousel', []) if (nextSlide.slide.index !== currentIndex && !$scope.$currentTransition) { goNext(nextSlide.slide, nextIndex, direction); - } else if (nextSlide && nextSlide.slide.index !== currentIndex && $scope.$currentTransition) { - bufferedTransitions.push(slides[nextIndex]); } }; @@ -611,12 +603,6 @@ angular.module('ui.bootstrap.carousel', []) } }); - function clearBufferedTransitions() { - while (bufferedTransitions.length) { - bufferedTransitions.shift(); - } - } - function getSlideByIndex(index) { for (var i = 0, l = slides.length; i < l; ++i) { if (slides[i].index === index) { @@ -652,14 +638,6 @@ angular.module('ui.bootstrap.carousel', []) if (phase === 'close') { $scope.$currentTransition = null; $animate.off('addClass', element); - if (bufferedTransitions.length) { - var nextSlide = bufferedTransitions.pop().slide; - var nextIndex = nextSlide.index; - var nextDirection = nextIndex > self.getCurrentIndex() ? 'next' : 'prev'; - clearBufferedTransitions(); - - goNext(nextSlide, nextIndex, nextDirection); - } } }); } @@ -690,7 +668,6 @@ angular.module('ui.bootstrap.carousel', []) function resetTransition(slides) { if (!slides.length) { $scope.$currentTransition = null; - clearBufferedTransitions(); } } @@ -811,7 +788,7 @@ function($animateCss) { angular.module('ui.bootstrap.dateparser', []) -.service('uibDateParser', ['$log', '$locale', 'dateFilter', 'orderByFilter', function($log, $locale, dateFilter, orderByFilter) { +.service('uibDateParser', ['$log', '$locale', 'dateFilter', 'orderByFilter', 'filterFilter', function($log, $locale, dateFilter, orderByFilter, filterFilter) { // Pulled from https://github.com/mbostock/d3/blob/master/src/format/requote.js var SPECIAL_CHARACTERS_REGEXP = /[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g; @@ -1041,10 +1018,36 @@ angular.module('ui.bootstrap.dateparser', []) formatter: function(date) { return dateFilter(date, 'G'); } } ]; + + if (angular.version.major >= 1 && angular.version.minor > 4) { + formatCodeToRegex.push({ + key: 'LLLL', + regex: $locale.DATETIME_FORMATS.STANDALONEMONTH.join('|'), + apply: function(value) { this.month = $locale.DATETIME_FORMATS.STANDALONEMONTH.indexOf(value); }, + formatter: function(date) { return dateFilter(date, 'LLLL'); } + }); + } }; this.init(); + function getFormatCodeToRegex(key) { + return filterFilter(formatCodeToRegex, {key: key}, true)[0]; + } + + this.getParser = function (key) { + var f = getFormatCodeToRegex(key); + return f && f.apply || null; + }; + + this.overrideParser = function (key, parser) { + var f = getFormatCodeToRegex(key); + if (f && angular.isFunction(parser)) { + this.parsers = {}; + f.apply = parser; + } + }.bind(this); + function createParser(format) { var map = [], regex = format.split(''); @@ -1526,7 +1529,7 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst $scope.$watch('datepickerOptions.' + key, function(value) { if (value) { if (angular.isDate(value)) { - self[key] = dateParser.fromTimezone(new Date(value), ngModelOptions.timezone); + self[key] = dateParser.fromTimezone(new Date(value), ngModelOptions.getOption('timezone')); } else { if ($datepickerLiteralWarning) { $log.warn('Literal date support has been deprecated, please switch to date object usage'); @@ -1536,7 +1539,7 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst } } else { self[key] = datepickerConfig[key] ? - dateParser.fromTimezone(new Date(datepickerConfig[key]), ngModelOptions.timezone) : + dateParser.fromTimezone(new Date(datepickerConfig[key]), ngModelOptions.getOption('timezone')) : null; } @@ -1583,14 +1586,13 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst this.init = function(ngModelCtrl_) { ngModelCtrl = ngModelCtrl_; - ngModelOptions = ngModelCtrl_.$options || - $scope.datepickerOptions.ngModelOptions || - datepickerConfig.ngModelOptions; + ngModelOptions = extractOptions(ngModelCtrl); + if ($scope.datepickerOptions.initDate) { - self.activeDate = dateParser.fromTimezone($scope.datepickerOptions.initDate, ngModelOptions.timezone) || new Date(); + self.activeDate = dateParser.fromTimezone($scope.datepickerOptions.initDate, ngModelOptions.getOption('timezone')) || new Date(); $scope.$watch('datepickerOptions.initDate', function(initDate) { if (initDate && (ngModelCtrl.$isEmpty(ngModelCtrl.$modelValue) || ngModelCtrl.$invalid)) { - self.activeDate = dateParser.fromTimezone(initDate, ngModelOptions.timezone); + self.activeDate = dateParser.fromTimezone(initDate, ngModelOptions.getOption('timezone')); self.refreshView(); } }); @@ -1600,8 +1602,8 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst var date = ngModelCtrl.$modelValue ? new Date(ngModelCtrl.$modelValue) : new Date(); this.activeDate = !isNaN(date) ? - dateParser.fromTimezone(date, ngModelOptions.timezone) : - dateParser.fromTimezone(new Date(), ngModelOptions.timezone); + dateParser.fromTimezone(date, ngModelOptions.getOption('timezone')) : + dateParser.fromTimezone(new Date(), ngModelOptions.getOption('timezone')); ngModelCtrl.$render = function() { self.render(); @@ -1614,7 +1616,7 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst isValid = !isNaN(date); if (isValid) { - this.activeDate = dateParser.fromTimezone(date, ngModelOptions.timezone); + this.activeDate = dateParser.fromTimezone(date, ngModelOptions.getOption('timezone')); } else if (!$datepickerSuppressError) { $log.error('Datepicker directive: "ng-model" value must be a Date object'); } @@ -1631,7 +1633,7 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst } var date = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : null; - date = dateParser.fromTimezone(date, ngModelOptions.timezone); + date = dateParser.fromTimezone(date, ngModelOptions.getOption('timezone')); ngModelCtrl.$setValidity('dateDisabled', !date || this.element && !this.isDisabled(date)); } @@ -1639,9 +1641,9 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst this.createDateObject = function(date, format) { var model = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : null; - model = dateParser.fromTimezone(model, ngModelOptions.timezone); + model = dateParser.fromTimezone(model, ngModelOptions.getOption('timezone')); var today = new Date(); - today = dateParser.fromTimezone(today, ngModelOptions.timezone); + today = dateParser.fromTimezone(today, ngModelOptions.getOption('timezone')); var time = this.compare(date, today); var dt = { date: date, @@ -1687,9 +1689,9 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst $scope.select = function(date) { if ($scope.datepickerMode === self.minMode) { - var dt = ngModelCtrl.$viewValue ? dateParser.fromTimezone(new Date(ngModelCtrl.$viewValue), ngModelOptions.timezone) : new Date(0, 0, 0, 0, 0, 0, 0); + var dt = ngModelCtrl.$viewValue ? dateParser.fromTimezone(new Date(ngModelCtrl.$viewValue), ngModelOptions.getOption('timezone')) : new Date(0, 0, 0, 0, 0, 0, 0); dt.setFullYear(date.getFullYear(), date.getMonth(), date.getDate()); - dt = dateParser.toTimezone(dt, ngModelOptions.timezone); + dt = dateParser.toTimezone(dt, ngModelOptions.getOption('timezone')); ngModelCtrl.$setViewValue(dt); ngModelCtrl.$render(); } else { @@ -1774,6 +1776,37 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst $scope.datepickerMode = mode; $scope.datepickerOptions.datepickerMode = mode; } + + function extractOptions(ngModelCtrl) { + var ngModelOptions; + + if (angular.version.minor < 6) { // in angular < 1.6 $options could be missing + // guarantee a value + ngModelOptions = ngModelCtrl.$options || + $scope.datepickerOptions.ngModelOptions || + datepickerConfig.ngModelOptions || + {}; + + // mimic 1.6+ api + ngModelOptions.getOption = function (key) { + return ngModelOptions[key]; + }; + } else { // in angular >=1.6 $options is always present + // ng-model-options defaults timezone to null; don't let its precedence squash a non-null value + var timezone = ngModelCtrl.$options.getOption('timezone') || + ($scope.datepickerOptions.ngModelOptions ? $scope.datepickerOptions.ngModelOptions.timezone : null) || + (datepickerConfig.ngModelOptions ? datepickerConfig.ngModelOptions.timezone : null); + + // values passed to createChild override existing values + ngModelOptions = ngModelCtrl.$options // start with a ModelOptions instance + .createChild(datepickerConfig.ngModelOptions) // lowest precedence + .createChild($scope.datepickerOptions.ngModelOptions) + .createChild(ngModelCtrl.$options) // highest precedence + .createChild({timezone: timezone}); // to keep from squashing a non-null value + } + + return ngModelOptions; + } }]) .controller('UibDaypickerController', ['$scope', '$element', 'dateFilter', function(scope, $element, dateFilter) { @@ -2208,7 +2241,7 @@ angular.module('ui.bootstrap.position', []) var paddingRight = this.parseStyle(elemStyle.paddingRight); var paddingBottom = this.parseStyle(elemStyle.paddingBottom); var scrollParent = this.scrollParent(elem, false, true); - var scrollbarWidth = this.scrollbarWidth(scrollParent, BODY_REGEX.test(scrollParent.tagName)); + var scrollbarWidth = this.scrollbarWidth(BODY_REGEX.test(scrollParent.tagName)); return { scrollbarWidth: scrollbarWidth, @@ -2731,11 +2764,7 @@ function($scope, $element, $attrs, $compile, $log, $parse, $window, $document, $ this.init = function(_ngModel_) { ngModel = _ngModel_; - ngModelOptions = angular.isObject(_ngModel_.$options) ? - _ngModel_.$options : - { - timezone: null - }; + ngModelOptions = extractOptions(ngModel); closeOnDateSelection = angular.isDefined($attrs.closeOnDateSelection) ? $scope.$parent.$eval($attrs.closeOnDateSelection) : datepickerPopupConfig.closeOnDateSelection; @@ -2826,13 +2855,13 @@ function($scope, $element, $attrs, $compile, $log, $parse, $window, $document, $ value = new Date(value); } - $scope.date = dateParser.fromTimezone(value, ngModelOptions.timezone); + $scope.date = dateParser.fromTimezone(value, ngModelOptions.getOption('timezone')); return dateParser.filter($scope.date, dateFormat); }); } else { ngModel.$formatters.push(function(value) { - $scope.date = dateParser.fromTimezone(value, ngModelOptions.timezone); + $scope.date = dateParser.fromTimezone(value, ngModelOptions.getOption('timezone')); return value; }); } @@ -2884,7 +2913,7 @@ function($scope, $element, $attrs, $compile, $log, $parse, $window, $document, $ $scope.isDisabled = function(date) { if (date === 'today') { - date = dateParser.fromTimezone(new Date(), ngModelOptions.timezone); + date = dateParser.fromTimezone(new Date(), ngModelOptions.getOption('timezone')); } var dates = {}; @@ -2941,7 +2970,7 @@ function($scope, $element, $attrs, $compile, $log, $parse, $window, $document, $ date = new Date($scope.date); date.setFullYear(today.getFullYear(), today.getMonth(), today.getDate()); } else { - date = dateParser.fromTimezone(today, ngModelOptions.timezone); + date = dateParser.fromTimezone(today, ngModelOptions.getOption('timezone')); date.setHours(0, 0, 0, 0); } } @@ -3032,11 +3061,11 @@ function($scope, $element, $attrs, $compile, $log, $parse, $window, $document, $ if (angular.isString(viewValue)) { var date = parseDateString(viewValue); if (!isNaN(date)) { - return dateParser.fromTimezone(date, ngModelOptions.timezone); + return dateParser.toTimezone(date, ngModelOptions.getOption('timezone')); } } - return ngModel.$options && ngModel.$options.allowInvalid ? viewValue : undefined; + return ngModelOptions.getOption('allowInvalid') ? viewValue : undefined; } function validator(modelValue, viewValue) { @@ -3111,6 +3140,28 @@ function($scope, $element, $attrs, $compile, $log, $parse, $window, $document, $ } } + function extractOptions(ngModelCtrl) { + var ngModelOptions; + + if (angular.version.minor < 6) { // in angular < 1.6 $options could be missing + // guarantee a value + ngModelOptions = angular.isObject(ngModelCtrl.$options) ? + ngModelCtrl.$options : + { + timezone: null + }; + + // mimic 1.6+ api + ngModelOptions.getOption = function (key) { + return ngModelOptions[key]; + }; + } else { // in angular >=1.6 $options is always present + ngModelOptions = ngModelCtrl.$options; + } + + return ngModelOptions; + } + $scope.$on('uib:datepicker.mode', function() { $timeout(positionPopup, 0, false); }); @@ -3168,17 +3219,92 @@ angular.module('ui.bootstrap.debounce', []) }; }]); -angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position']) +angular.module('ui.bootstrap.multiMap', []) +/** + * A helper, internal data structure that stores all references attached to key + */ + .factory('$$multiMap', function() { + return { + createNew: function() { + var map = {}; + + return { + entries: function() { + return Object.keys(map).map(function(key) { + return { + key: key, + value: map[key] + }; + }); + }, + get: function(key) { + return map[key]; + }, + hasKey: function(key) { + return !!map[key]; + }, + keys: function() { + return Object.keys(map); + }, + put: function(key, value) { + if (!map[key]) { + map[key] = []; + } + + map[key].push(value); + }, + remove: function(key, value) { + var values = map[key]; + + if (!values) { + return; + } + + var idx = values.indexOf(value); + + if (idx !== -1) { + values.splice(idx, 1); + } + + if (!values.length) { + delete map[key]; + } + } + }; + } + }; + }); + +angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.multiMap', 'ui.bootstrap.position']) .constant('uibDropdownConfig', { appendToOpenClass: 'uib-dropdown-open', openClass: 'open' }) -.service('uibDropdownService', ['$document', '$rootScope', function($document, $rootScope) { +.service('uibDropdownService', ['$document', '$rootScope', '$$multiMap', function($document, $rootScope, $$multiMap) { var openScope = null; + var openedContainers = $$multiMap.createNew(); + + this.isOnlyOpen = function(dropdownScope, appendTo) { + var openedDropdowns = openedContainers.get(appendTo); + if (openedDropdowns) { + var openDropdown = openedDropdowns.reduce(function(toClose, dropdown) { + if (dropdown.scope === dropdownScope) { + return dropdown; + } - this.open = function(dropdownScope, element) { + return toClose; + }, {}); + if (openDropdown) { + return openedDropdowns.length === 1; + } + } + + return false; + }; + + this.open = function(dropdownScope, element, appendTo) { if (!openScope) { $document.on('click', closeDropdown); } @@ -3188,20 +3314,58 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position']) } openScope = dropdownScope; + + if (!appendTo) { + return; + } + + var openedDropdowns = openedContainers.get(appendTo); + if (openedDropdowns) { + var openedScopes = openedDropdowns.map(function(dropdown) { + return dropdown.scope; + }); + if (openedScopes.indexOf(dropdownScope) === -1) { + openedContainers.put(appendTo, { + scope: dropdownScope + }); + } + } else { + openedContainers.put(appendTo, { + scope: dropdownScope + }); + } }; - this.close = function(dropdownScope, element) { + this.close = function(dropdownScope, element, appendTo) { if (openScope === dropdownScope) { - openScope = null; $document.off('click', closeDropdown); $document.off('keydown', this.keybindFilter); + openScope = null; + } + + if (!appendTo) { + return; + } + + var openedDropdowns = openedContainers.get(appendTo); + if (openedDropdowns) { + var dropdownToClose = openedDropdowns.reduce(function(toClose, dropdown) { + if (dropdown.scope === dropdownScope) { + return dropdown; + } + + return toClose; + }, {}); + if (dropdownToClose) { + openedContainers.remove(appendTo, dropdownToClose); + } } }; var closeDropdown = function(evt) { // This method may still be called during the same mouse event that // unbound this event handler. So check openScope before proceeding. - if (!openScope) { return; } + if (!openScope || !openScope.isOpen) { return; } if (evt && openScope.getAutoClose() === 'disabled') { return; } @@ -3218,8 +3382,8 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position']) return; } - openScope.isOpen = false; openScope.focusToggleElement(); + openScope.isOpen = false; if (!$rootScope.$$phase) { openScope.$apply(); @@ -3227,6 +3391,11 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position']) }; this.keybindFilter = function(evt) { + if (!openScope) { + // see this.close as ESC could have been pressed which kills the scope so we can not proceed + return; + } + var dropdownElement = openScope.getDropdownElement(); var toggleElement = openScope.getToggleElement(); var dropdownElementTargeted = dropdownElement && dropdownElement[0].contains(evt.target); @@ -3252,8 +3421,6 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position']) getIsOpen, setIsOpen = angular.noop, toggleInvoker = $attrs.onToggle ? $parse($attrs.onToggle) : angular.noop, - appendToBody = false, - appendTo = null, keynavEnabled = false, selectedOption = null, body = $document.find('body'); @@ -3270,26 +3437,7 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position']) }); } - if (angular.isDefined($attrs.dropdownAppendTo)) { - var appendToEl = $parse($attrs.dropdownAppendTo)(scope); - if (appendToEl) { - appendTo = angular.element(appendToEl); - } - } - - appendToBody = angular.isDefined($attrs.dropdownAppendToBody); keynavEnabled = angular.isDefined($attrs.keyboardNav); - - if (appendToBody && !appendTo) { - appendTo = body; - } - - if (appendTo && self.dropdownMenu) { - appendTo.append(self.dropdownMenu); - $element.on('$destroy', function handleDestroyEvent() { - self.dropdownMenu.remove(); - }); - } }; this.toggle = function(open) { @@ -3361,7 +3509,42 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position']) } }; + function removeDropdownMenu() { + $element.append(self.dropdownMenu); + } + scope.$watch('isOpen', function(isOpen, wasOpen) { + var appendTo = null, + appendToBody = false; + + if (angular.isDefined($attrs.dropdownAppendTo)) { + var appendToEl = $parse($attrs.dropdownAppendTo)(scope); + if (appendToEl) { + appendTo = angular.element(appendToEl); + } + } + + if (angular.isDefined($attrs.dropdownAppendToBody)) { + var appendToBodyValue = $parse($attrs.dropdownAppendToBody)(scope); + if (appendToBodyValue !== false) { + appendToBody = true; + } + } + + if (appendToBody && !appendTo) { + appendTo = body; + } + + if (appendTo && self.dropdownMenu) { + if (isOpen) { + appendTo.append(self.dropdownMenu); + $element.on('$destroy', removeDropdownMenu); + } else { + $element.off('$destroy', removeDropdownMenu); + removeDropdownMenu(); + } + } + if (appendTo && self.dropdownMenu) { var pos = $position.positionElements($element, self.dropdownMenu, 'bottom-left', true), css, @@ -3409,10 +3592,18 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position']) } var openContainer = appendTo ? appendTo : $element; - var hasOpenClass = openContainer.hasClass(appendTo ? appendToOpenClass : openClass); + var dropdownOpenClass = appendTo ? appendToOpenClass : openClass; + var hasOpenClass = openContainer.hasClass(dropdownOpenClass); + var isOnlyOpen = uibDropdownService.isOnlyOpen($scope, appendTo); if (hasOpenClass === !isOpen) { - $animate[isOpen ? 'addClass' : 'removeClass'](openContainer, appendTo ? appendToOpenClass : openClass).then(function() { + var toggleClass; + if (appendTo) { + toggleClass = !isOnlyOpen ? 'addClass' : 'removeClass'; + } else { + toggleClass = isOpen ? 'addClass' : 'removeClass'; + } + $animate[toggleClass](openContainer, dropdownOpenClass).then(function() { if (angular.isDefined(isOpen) && isOpen !== wasOpen) { toggleInvoker($scope, { open: !!isOpen }); } @@ -3435,9 +3626,9 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position']) } scope.focusToggleElement(); - uibDropdownService.open(scope, $element); + uibDropdownService.open(scope, $element, appendTo); } else { - uibDropdownService.close(scope, $element); + uibDropdownService.close(scope, $element, appendTo); if (self.dropdownMenuTemplateUrl) { if (templateScope) { templateScope.$destroy(); @@ -3510,7 +3701,7 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position']) } }; - element.bind('click', toggleDropdown); + element.on('click', toggleDropdown); // WAI-ARIA element.attr({ 'aria-haspopup': true, 'aria-expanded': false }); @@ -3519,7 +3710,7 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position']) }); scope.$on('$destroy', function() { - element.unbind('click', toggleDropdown); + element.off('click', toggleDropdown); }); } }; @@ -3579,62 +3770,7 @@ angular.module('ui.bootstrap.stackedMap', []) } }; }); -angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap', 'ui.bootstrap.position']) -/** - * A helper, internal data structure that stores all references attached to key - */ - .factory('$$multiMap', function() { - return { - createNew: function() { - var map = {}; - - return { - entries: function() { - return Object.keys(map).map(function(key) { - return { - key: key, - value: map[key] - }; - }); - }, - get: function(key) { - return map[key]; - }, - hasKey: function(key) { - return !!map[key]; - }, - keys: function() { - return Object.keys(map); - }, - put: function(key, value) { - if (!map[key]) { - map[key] = []; - } - - map[key].push(value); - }, - remove: function(key, value) { - var values = map[key]; - - if (!values) { - return; - } - - var idx = values.indexOf(value); - - if (idx !== -1) { - values.splice(idx, 1); - } - - if (!values.length) { - delete map[key]; - } - } - }; - } - }; - }) - +angular.module('ui.bootstrap.modal', ['ui.bootstrap.multiMap', 'ui.bootstrap.stackedMap', 'ui.bootstrap.position']) /** * Pluggable resolve mechanism for the modal resolve resolution * Supports UI Router's $resolve service @@ -3744,7 +3880,7 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap', 'ui.bootstrap.p // {@link Attribute#$observe} on it. For more details please see {@link TableColumnResize}. scope.$isRendered = true; - // Deferred object that will be resolved when this modal is render. + // Deferred object that will be resolved when this modal is rendered. var modalRenderDeferObj = $q.defer(); // Resolve render promise post-digest scope.$$postDigest(function() { @@ -3777,7 +3913,7 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap', 'ui.bootstrap.p /** * If something within the freshly-opened modal already has focus (perhaps via a - * directive that causes focus). then no need to try and focus anything. + * directive that causes focus) then there's no need to try to focus anything. */ if (!($document[0].activeElement && element[0].contains($document[0].activeElement))) { var inputWithAutofocus = element[0].querySelector('[autofocus]'); @@ -3835,6 +3971,7 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap', 'ui.bootstrap.p }; var topModalIndex = 0; var previousTopOpenedModal = null; + var ARIA_HIDDEN_ATTRIBUTE_NAME = 'data-bootstrap-modal-aria-hidden-count'; //Modal focus behavior var tabbableSelector = 'a[href], area[href], input:not([disabled]):not([tabindex=\'-1\']), ' + @@ -4054,10 +4191,6 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap', 'ui.bootstrap.p var appendToElement = modal.appendTo, currBackdropIndex = backdropIndex(); - if (!appendToElement.length) { - throw new Error('appendTo element not found. Make sure that the element passed is in DOM.'); - } - if (currBackdropIndex >= 0 && !backdropDomEl) { backdropScope = $rootScope.$new(true); backdropScope.modalOptions = modal; @@ -4136,25 +4269,74 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap', 'ui.bootstrap.p openedWindows.top().value.modalDomEl = angularDomEl; openedWindows.top().value.modalOpener = modalOpener; + + applyAriaHidden(angularDomEl); + + function applyAriaHidden(el) { + if (!el || el[0].tagName === 'BODY') { + return; + } + + getSiblings(el).forEach(function(sibling) { + var elemIsAlreadyHidden = sibling.getAttribute('aria-hidden') === 'true', + ariaHiddenCount = parseInt(sibling.getAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME), 10); + + if (!ariaHiddenCount) { + ariaHiddenCount = elemIsAlreadyHidden ? 1 : 0; + } + + sibling.setAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME, ariaHiddenCount + 1); + sibling.setAttribute('aria-hidden', 'true'); + }); + + return applyAriaHidden(el.parent()); + + function getSiblings(el) { + var children = el.parent() ? el.parent().children() : []; + + return Array.prototype.filter.call(children, function(child) { + return child !== el[0]; + }); + } + } }; function broadcastClosing(modalWindow, resultOrReason, closing) { return !modalWindow.value.modalScope.$broadcast('modal.closing', resultOrReason, closing).defaultPrevented; } + function unhideBackgroundElements() { + Array.prototype.forEach.call( + document.querySelectorAll('[' + ARIA_HIDDEN_ATTRIBUTE_NAME + ']'), + function(hiddenEl) { + var ariaHiddenCount = parseInt(hiddenEl.getAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME), 10), + newHiddenCount = ariaHiddenCount - 1; + hiddenEl.setAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME, newHiddenCount); + + if (!newHiddenCount) { + hiddenEl.removeAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME); + hiddenEl.removeAttribute('aria-hidden'); + } + } + ); + } + $modalStack.close = function(modalInstance, result) { var modalWindow = openedWindows.get(modalInstance); + unhideBackgroundElements(); if (modalWindow && broadcastClosing(modalWindow, result, true)) { modalWindow.value.modalScope.$$uibDestructionScheduled = true; modalWindow.value.deferred.resolve(result); removeModalWindow(modalInstance, modalWindow.value.modalOpener); return true; } + return !modalWindow; }; $modalStack.dismiss = function(modalInstance, reason) { var modalWindow = openedWindows.get(modalInstance); + unhideBackgroundElements(); if (modalWindow && broadcastClosing(modalWindow, reason, false)) { modalWindow.value.modalScope.$$uibDestructionScheduled = true; modalWindow.value.deferred.reject(reason); @@ -4285,6 +4467,10 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap', 'ui.bootstrap.p modalOptions.resolve = modalOptions.resolve || {}; modalOptions.appendTo = modalOptions.appendTo || $document.find('body').eq(0); + if (!modalOptions.appendTo.length) { + throw new Error('appendTo element not found. Make sure that the element passed is in DOM.'); + } + //verify options if (!modalOptions.component && !modalOptions.template && !modalOptions.templateUrl) { throw new Error('One of component or template or templateUrl options is required.'); @@ -4562,6 +4748,7 @@ angular.module('ui.bootstrap.pagination', ['ui.bootstrap.paging', 'ui.bootstrap. pageLabel = angular.isDefined($attrs.pageLabel) ? function(idx) { return $scope.$parent.$eval($attrs.pageLabel, {$page: idx}); } : angular.identity; $scope.boundaryLinks = angular.isDefined($attrs.boundaryLinks) ? $scope.$parent.$eval($attrs.boundaryLinks) : uibPaginationConfig.boundaryLinks; $scope.directionLinks = angular.isDefined($attrs.directionLinks) ? $scope.$parent.$eval($attrs.directionLinks) : uibPaginationConfig.directionLinks; + $attrs.$set('role', 'menu'); uibPaging.create(this, $scope, $attrs); @@ -4850,6 +5037,7 @@ angular.module('ui.bootstrap.tooltip', ['ui.bootstrap.position', 'ui.bootstrap.s var showTimeout; var hideTimeout; var positionTimeout; + var adjustmentTimeout; var appendToBody = angular.isDefined(options.appendToBody) ? options.appendToBody : false; var triggers = getTriggers(undefined); var hasEnableExp = angular.isDefined(attrs[prefix + 'Enable']); @@ -4882,12 +5070,13 @@ angular.module('ui.bootstrap.tooltip', ['ui.bootstrap.position', 'ui.bootstrap.s tooltip.addClass(options.placementClassPrefix + ttPosition.placement); } - $timeout(function() { + adjustmentTimeout = $timeout(function() { var currentHeight = angular.isDefined(tooltip.offsetHeight) ? tooltip.offsetHeight : tooltip.prop('offsetHeight'); var adjustment = $position.adjustTop(placementClasses, elementPos, initialHeight, currentHeight); if (adjustment) { tooltip.css(adjustment); } + adjustmentTimeout = null; }, 0, false); // first time through tt element will have the @@ -5052,7 +5241,11 @@ angular.module('ui.bootstrap.tooltip', ['ui.bootstrap.position', 'ui.bootstrap.s if (tooltip) { tooltip.remove(); + tooltip = null; + if (adjustmentTimeout) { + $timeout.cancel(adjustmentTimeout); + } } openedTooltips.remove(ttScope); @@ -5196,6 +5389,13 @@ angular.module('ui.bootstrap.tooltip', ['ui.bootstrap.position', 'ui.bootstrap.s } } + // KeyboardEvent handler to hide the tooltip on Escape key press + function hideOnEscapeKey(e) { + if (e.which === 27) { + hideTooltipBind(); + } + } + var unregisterTriggers = function() { triggers.show.forEach(function(trigger) { if (trigger === 'outsideClick') { @@ -5204,6 +5404,7 @@ angular.module('ui.bootstrap.tooltip', ['ui.bootstrap.position', 'ui.bootstrap.s element.off(trigger, showTooltipBind); element.off(trigger, toggleTooltipBind); } + element.off('keypress', hideOnEscapeKey); }); triggers.hide.forEach(function(trigger) { if (trigger === 'outsideClick') { @@ -5243,12 +5444,7 @@ angular.module('ui.bootstrap.tooltip', ['ui.bootstrap.position', 'ui.bootstrap.s element.on(trigger, showTooltipBind); element.on(triggers.hide[idx], hideTooltipBind); } - - element.on('keypress', function(e) { - if (e.which === 27) { - hideTooltipBind(); - } - }); + element.on('keypress', hideOnEscapeKey); }); } } @@ -5940,6 +6136,7 @@ angular.module('ui.bootstrap.timepicker', []) }) .controller('UibTimepickerController', ['$scope', '$element', '$attrs', '$parse', '$log', '$locale', 'uibTimepickerConfig', function($scope, $element, $attrs, $parse, $log, $locale, timepickerConfig) { + var hoursModelCtrl, minutesModelCtrl, secondsModelCtrl; var selected = new Date(), watchers = [], ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl @@ -5961,6 +6158,10 @@ angular.module('ui.bootstrap.timepicker', []) minutesInputEl = inputs.eq(1), secondsInputEl = inputs.eq(2); + hoursModelCtrl = hoursInputEl.controller('ngModel'); + minutesModelCtrl = minutesInputEl.controller('ngModel'); + secondsModelCtrl = secondsInputEl.controller('ngModel'); + var mousewheel = angular.isDefined($attrs.mousewheel) ? $scope.$parent.$eval($attrs.mousewheel) : timepickerConfig.mousewheel; if (mousewheel) { @@ -6140,21 +6341,21 @@ angular.module('ui.bootstrap.timepicker', []) return e.detail || delta > 0; }; - hoursInputEl.bind('mousewheel wheel', function(e) { + hoursInputEl.on('mousewheel wheel', function(e) { if (!disabled) { $scope.$apply(isScrollingUp(e) ? $scope.incrementHours() : $scope.decrementHours()); } e.preventDefault(); }); - minutesInputEl.bind('mousewheel wheel', function(e) { + minutesInputEl.on('mousewheel wheel', function(e) { if (!disabled) { $scope.$apply(isScrollingUp(e) ? $scope.incrementMinutes() : $scope.decrementMinutes()); } e.preventDefault(); }); - secondsInputEl.bind('mousewheel wheel', function(e) { + secondsInputEl.on('mousewheel wheel', function(e) { if (!disabled) { $scope.$apply(isScrollingUp(e) ? $scope.incrementSeconds() : $scope.decrementSeconds()); } @@ -6164,7 +6365,7 @@ angular.module('ui.bootstrap.timepicker', []) // Respond on up/down arrowkeys this.setupArrowkeyEvents = function(hoursInputEl, minutesInputEl, secondsInputEl) { - hoursInputEl.bind('keydown', function(e) { + hoursInputEl.on('keydown', function(e) { if (!disabled) { if (e.which === 38) { // up e.preventDefault(); @@ -6178,7 +6379,7 @@ angular.module('ui.bootstrap.timepicker', []) } }); - minutesInputEl.bind('keydown', function(e) { + minutesInputEl.on('keydown', function(e) { if (!disabled) { if (e.which === 38) { // up e.preventDefault(); @@ -6192,7 +6393,7 @@ angular.module('ui.bootstrap.timepicker', []) } }); - secondsInputEl.bind('keydown', function(e) { + secondsInputEl.on('keydown', function(e) { if (!disabled) { if (e.which === 38) { // up e.preventDefault(); @@ -6220,14 +6421,23 @@ angular.module('ui.bootstrap.timepicker', []) ngModelCtrl.$setValidity('time', false); if (angular.isDefined(invalidHours)) { $scope.invalidHours = invalidHours; + if (hoursModelCtrl) { + hoursModelCtrl.$setValidity('hours', false); + } } if (angular.isDefined(invalidMinutes)) { $scope.invalidMinutes = invalidMinutes; + if (minutesModelCtrl) { + minutesModelCtrl.$setValidity('minutes', false); + } } if (angular.isDefined(invalidSeconds)) { $scope.invalidSeconds = invalidSeconds; + if (secondsModelCtrl) { + secondsModelCtrl.$setValidity('seconds', false); + } } }; @@ -6250,7 +6460,7 @@ angular.module('ui.bootstrap.timepicker', []) } }; - hoursInputEl.bind('blur', function(e) { + hoursInputEl.on('blur', function(e) { ngModelCtrl.$setTouched(); if (modelIsEmpty()) { makeValid(); @@ -6282,7 +6492,7 @@ angular.module('ui.bootstrap.timepicker', []) } }; - minutesInputEl.bind('blur', function(e) { + minutesInputEl.on('blur', function(e) { ngModelCtrl.$setTouched(); if (modelIsEmpty()) { makeValid(); @@ -6308,7 +6518,7 @@ angular.module('ui.bootstrap.timepicker', []) } }; - secondsInputEl.bind('blur', function(e) { + secondsInputEl.on('blur', function(e) { if (modelIsEmpty()) { makeValid(); } else if (!$scope.invalidSeconds && $scope.seconds < 10) { @@ -6350,6 +6560,18 @@ angular.module('ui.bootstrap.timepicker', []) } function makeValid() { + if (hoursModelCtrl) { + hoursModelCtrl.$setValidity('hours', true); + } + + if (minutesModelCtrl) { + minutesModelCtrl.$setValidity('minutes', true); + } + + if (secondsModelCtrl) { + secondsModelCtrl.$setValidity('seconds', true); + } + ngModelCtrl.$setValidity('time', true); $scope.invalidHours = false; $scope.invalidMinutes = false; @@ -6585,7 +6807,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap var invokeModelSetter = $parse(attrs.ngModel + '($$$p)'); var $setModelValue = function(scope, newValue) { if (angular.isFunction(parsedModel(originalScope)) && - ngModelOptions && ngModelOptions.$options && ngModelOptions.$options.getterSetter) { + ngModelOptions.getOption('getterSetter')) { return invokeModelSetter(scope, {$$$p: newValue}); } @@ -6921,7 +7143,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap } }); - element.bind('focus', function (evt) { + element.on('focus', function (evt) { hasFocus = true; if (minLength === 0 && !modelCtrl.$viewValue) { $timeout(function() { @@ -6930,7 +7152,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap } }); - element.bind('blur', function(evt) { + element.on('blur', function(evt) { if (isSelectOnBlur && scope.matches.length && scope.activeIdx !== -1 && !selected) { selected = true; scope.$apply(function() { @@ -6998,11 +7220,11 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap element.after($popup); } - this.init = function(_modelCtrl, _ngModelOptions) { + this.init = function(_modelCtrl) { modelCtrl = _modelCtrl; - ngModelOptions = _ngModelOptions; + ngModelOptions = extractOptions(modelCtrl); - scope.debounceUpdate = modelCtrl.$options && $parse(modelCtrl.$options.debounce)(originalScope); + scope.debounceUpdate = $parse(ngModelOptions.getOption('debounce'))(originalScope); //plug into $parsers pipeline to open a typeahead on view changes initiated from DOM //$parsers kick-in on all the changes coming from the view as well as manually triggered by $setViewValue @@ -7062,14 +7284,32 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap return candidateViewValue !== emptyViewValue ? candidateViewValue : modelValue; }); }; + + function extractOptions(ngModelCtrl) { + var ngModelOptions; + + if (angular.version.minor < 6) { // in angular < 1.6 $options could be missing + // guarantee a value + ngModelOptions = ngModelCtrl.$options || {}; + + // mimic 1.6+ api + ngModelOptions.getOption = function (key) { + return ngModelOptions[key]; + }; + } else { // in angular >=1.6 $options is always present + ngModelOptions = ngModelCtrl.$options; + } + + return ngModelOptions; + } }]) .directive('uibTypeahead', function() { return { controller: 'UibTypeaheadController', - require: ['ngModel', '^?ngModelOptions', 'uibTypeahead'], + require: ['ngModel', 'uibTypeahead'], link: function(originalScope, element, attrs, ctrls) { - ctrls[2].init(ctrls[0], ctrls[1]); + ctrls[1].init(ctrls[0]); } }; }) @@ -7233,9 +7473,9 @@ angular.module("uib/template/datepicker/day.html", []).run(["$templateCache", fu "<table role=\"grid\" aria-labelledby=\"{{::uniqueId}}-title\" aria-activedescendant=\"{{activeDateId}}\">\n" + " <thead>\n" + " <tr>\n" + - " <th><button type=\"button\" class=\"btn btn-default btn-sm pull-left uib-left\" ng-click=\"move(-1)\" tabindex=\"-1\"><i class=\"glyphicon glyphicon-chevron-left\"></i></button></th>\n" + + " <th><button type=\"button\" class=\"btn btn-default btn-sm pull-left uib-left\" ng-click=\"move(-1)\" tabindex=\"-1\"><i aria-hidden=\"true\" class=\"glyphicon glyphicon-chevron-left\"></i><span class=\"sr-only\">previous</span></button></th>\n" + " <th colspan=\"{{::5 + showWeeks}}\"><button id=\"{{::uniqueId}}-title\" role=\"heading\" aria-live=\"assertive\" aria-atomic=\"true\" type=\"button\" class=\"btn btn-default btn-sm uib-title\" ng-click=\"toggleMode()\" ng-disabled=\"datepickerMode === maxMode\" tabindex=\"-1\"><strong>{{title}}</strong></button></th>\n" + - " <th><button type=\"button\" class=\"btn btn-default btn-sm pull-right uib-right\" ng-click=\"move(1)\" tabindex=\"-1\"><i class=\"glyphicon glyphicon-chevron-right\"></i></button></th>\n" + + " <th><button type=\"button\" class=\"btn btn-default btn-sm pull-right uib-right\" ng-click=\"move(1)\" tabindex=\"-1\"><i aria-hidden=\"true\" class=\"glyphicon glyphicon-chevron-right\"></i><span class=\"sr-only\">next</span></button></th>\n" + " </tr>\n" + " <tr>\n" + " <th ng-if=\"showWeeks\" class=\"text-center\"></th>\n" + @@ -7268,9 +7508,9 @@ angular.module("uib/template/datepicker/month.html", []).run(["$templateCache", "<table role=\"grid\" aria-labelledby=\"{{::uniqueId}}-title\" aria-activedescendant=\"{{activeDateId}}\">\n" + " <thead>\n" + " <tr>\n" + - " <th><button type=\"button\" class=\"btn btn-default btn-sm pull-left uib-left\" ng-click=\"move(-1)\" tabindex=\"-1\"><i class=\"glyphicon glyphicon-chevron-left\"></i></button></th>\n" + + " <th><button type=\"button\" class=\"btn btn-default btn-sm pull-left uib-left\" ng-click=\"move(-1)\" tabindex=\"-1\"><i aria-hidden=\"true\" class=\"glyphicon glyphicon-chevron-left\"></i><span class=\"sr-only\">previous</span></button></th>\n" + " <th colspan=\"{{::yearHeaderColspan}}\"><button id=\"{{::uniqueId}}-title\" role=\"heading\" aria-live=\"assertive\" aria-atomic=\"true\" type=\"button\" class=\"btn btn-default btn-sm uib-title\" ng-click=\"toggleMode()\" ng-disabled=\"datepickerMode === maxMode\" tabindex=\"-1\"><strong>{{title}}</strong></button></th>\n" + - " <th><button type=\"button\" class=\"btn btn-default btn-sm pull-right uib-right\" ng-click=\"move(1)\" tabindex=\"-1\"><i class=\"glyphicon glyphicon-chevron-right\"></i></button></th>\n" + + " <th><button type=\"button\" class=\"btn btn-default btn-sm pull-right uib-right\" ng-click=\"move(1)\" tabindex=\"-1\"><i aria-hidden=\"true\" class=\"glyphicon glyphicon-chevron-right\"></i><span class=\"sr-only\">next</span></i></button></th>\n" + " </tr>\n" + " </thead>\n" + " <tbody>\n" + @@ -7298,9 +7538,9 @@ angular.module("uib/template/datepicker/year.html", []).run(["$templateCache", f "<table role=\"grid\" aria-labelledby=\"{{::uniqueId}}-title\" aria-activedescendant=\"{{activeDateId}}\">\n" + " <thead>\n" + " <tr>\n" + - " <th><button type=\"button\" class=\"btn btn-default btn-sm pull-left uib-left\" ng-click=\"move(-1)\" tabindex=\"-1\"><i class=\"glyphicon glyphicon-chevron-left\"></i></button></th>\n" + + " <th><button type=\"button\" class=\"btn btn-default btn-sm pull-left uib-left\" ng-click=\"move(-1)\" tabindex=\"-1\"><i aria-hidden=\"true\" class=\"glyphicon glyphicon-chevron-left\"></i><span class=\"sr-only\">previous</span></button></th>\n" + " <th colspan=\"{{::columns - 2}}\"><button id=\"{{::uniqueId}}-title\" role=\"heading\" aria-live=\"assertive\" aria-atomic=\"true\" type=\"button\" class=\"btn btn-default btn-sm uib-title\" ng-click=\"toggleMode()\" ng-disabled=\"datepickerMode === maxMode\" tabindex=\"-1\"><strong>{{title}}</strong></button></th>\n" + - " <th><button type=\"button\" class=\"btn btn-default btn-sm pull-right uib-right\" ng-click=\"move(1)\" tabindex=\"-1\"><i class=\"glyphicon glyphicon-chevron-right\"></i></button></th>\n" + + " <th><button type=\"button\" class=\"btn btn-default btn-sm pull-right uib-right\" ng-click=\"move(1)\" tabindex=\"-1\"><i aria-hidden=\"true\" class=\"glyphicon glyphicon-chevron-right\"></i><span class=\"sr-only\">next</span></button></th>\n" + " </tr>\n" + " </thead>\n" + " <tbody>\n" + @@ -7325,7 +7565,7 @@ angular.module("uib/template/datepicker/year.html", []).run(["$templateCache", f angular.module("uib/template/datepickerPopup/popup.html", []).run(["$templateCache", function($templateCache) { $templateCache.put("uib/template/datepickerPopup/popup.html", - "<ul class=\"uib-datepicker-popup dropdown-menu uib-position-measure\" dropdown-nested ng-if=\"isOpen\" ng-keydown=\"keydown($event)\" ng-click=\"$event.stopPropagation()\">\n" + + "<ul role=\"presentation\" class=\"uib-datepicker-popup dropdown-menu uib-position-measure\" dropdown-nested ng-if=\"isOpen\" ng-keydown=\"keydown($event)\" ng-click=\"$event.stopPropagation()\">\n" + " <li ng-transclude></li>\n" + " <li ng-if=\"showButtonBar\" class=\"uib-button-bar\">\n" + " <span class=\"btn-group pull-left\">\n" + @@ -7353,11 +7593,11 @@ angular.module("uib/template/pager/pager.html", []).run(["$templateCache", funct angular.module("uib/template/pagination/pagination.html", []).run(["$templateCache", function($templateCache) { $templateCache.put("uib/template/pagination/pagination.html", - "<li ng-if=\"::boundaryLinks\" ng-class=\"{disabled: noPrevious()||ngDisabled}\" class=\"pagination-first\"><a href ng-click=\"selectPage(1, $event)\" ng-disabled=\"noPrevious()||ngDisabled\" uib-tabindex-toggle>{{::getText('first')}}</a></li>\n" + - "<li ng-if=\"::directionLinks\" ng-class=\"{disabled: noPrevious()||ngDisabled}\" class=\"pagination-prev\"><a href ng-click=\"selectPage(page - 1, $event)\" ng-disabled=\"noPrevious()||ngDisabled\" uib-tabindex-toggle>{{::getText('previous')}}</a></li>\n" + - "<li ng-repeat=\"page in pages track by $index\" ng-class=\"{active: page.active,disabled: ngDisabled&&!page.active}\" class=\"pagination-page\"><a href ng-click=\"selectPage(page.number, $event)\" ng-disabled=\"ngDisabled&&!page.active\" uib-tabindex-toggle>{{page.text}}</a></li>\n" + - "<li ng-if=\"::directionLinks\" ng-class=\"{disabled: noNext()||ngDisabled}\" class=\"pagination-next\"><a href ng-click=\"selectPage(page + 1, $event)\" ng-disabled=\"noNext()||ngDisabled\" uib-tabindex-toggle>{{::getText('next')}}</a></li>\n" + - "<li ng-if=\"::boundaryLinks\" ng-class=\"{disabled: noNext()||ngDisabled}\" class=\"pagination-last\"><a href ng-click=\"selectPage(totalPages, $event)\" ng-disabled=\"noNext()||ngDisabled\" uib-tabindex-toggle>{{::getText('last')}}</a></li>\n" + + "<li role=\"menuitem\" ng-if=\"::boundaryLinks\" ng-class=\"{disabled: noPrevious()||ngDisabled}\" class=\"pagination-first\"><a href ng-click=\"selectPage(1, $event)\" ng-disabled=\"noPrevious()||ngDisabled\" uib-tabindex-toggle>{{::getText('first')}}</a></li>\n" + + "<li role=\"menuitem\" ng-if=\"::directionLinks\" ng-class=\"{disabled: noPrevious()||ngDisabled}\" class=\"pagination-prev\"><a href ng-click=\"selectPage(page - 1, $event)\" ng-disabled=\"noPrevious()||ngDisabled\" uib-tabindex-toggle>{{::getText('previous')}}</a></li>\n" + + "<li role=\"menuitem\" ng-repeat=\"page in pages track by $index\" ng-class=\"{active: page.active,disabled: ngDisabled&&!page.active}\" class=\"pagination-page\"><a href ng-click=\"selectPage(page.number, $event)\" ng-disabled=\"ngDisabled&&!page.active\" uib-tabindex-toggle>{{page.text}}</a></li>\n" + + "<li role=\"menuitem\" ng-if=\"::directionLinks\" ng-class=\"{disabled: noNext()||ngDisabled}\" class=\"pagination-next\"><a href ng-click=\"selectPage(page + 1, $event)\" ng-disabled=\"noNext()||ngDisabled\" uib-tabindex-toggle>{{::getText('next')}}</a></li>\n" + + "<li role=\"menuitem\" ng-if=\"::boundaryLinks\" ng-class=\"{disabled: noNext()||ngDisabled}\" class=\"pagination-last\"><a href ng-click=\"selectPage(totalPages, $event)\" ng-disabled=\"noNext()||ngDisabled\" uib-tabindex-toggle>{{::getText('last')}}</a></li>\n" + ""); }]); diff --git a/static/js/versionController.js b/static/js/versionController.js index 65d9f492dc..9b6e9c9dd3 100644 --- a/static/js/versionController.js +++ b/static/js/versionController.js @@ -1,10 +1,8 @@ angular.module('bmcApp').controller('versionController', [ - '$scope', '$resource', - function($scope, $resource) { + '$scope', '$http', + function($scope, $http) { - - var systeminfo = $resource("/systeminfo"); - systeminfo.get(function(systeminfo){ + var systeminfo = $http.get("/systeminfo").then(function(systeminfo){ $scope.host_power_on= true; $scope.rmm_module_installed= true; $scope.bmc_available= true; @@ -15,7 +13,4 @@ angular.module('bmcApp').controller('versionController', [ $scope.bmc_backup_build_number = "96.37"; $scope.bmc_backup_build_extended = "e04989f7"; }); - - - }]);
\ No newline at end of file diff --git a/static/loginModalTemplate.html b/static/loginModalTemplate.html deleted file mode 100644 index 7bde942a7e..0000000000 --- a/static/loginModalTemplate.html +++ /dev/null @@ -1,10 +0,0 @@ -<!-- views/loginModalTemplate.html --> - -<div> - <form ng-submit="LoginModalCtrl.submit(_email, _password)"> - <input type="email" ng-model="_email" /> - <input type="password" ng-model="_password" /> - <button>Submit</button> - </form> - <button ng-click="LoginModalCtrl.cancel()">Cancel</button> -</div>
\ No newline at end of file diff --git a/static/partial-fruinfo.html b/static/partial-fruinfo.html index 6d7498324a..b35b7ca64d 100644 --- a/static/partial-fruinfo.html +++ b/static/partial-fruinfo.html @@ -1,6 +1,6 @@ -<div class="jumbotron text-center"> +<div class="jumbotron text-center container"> <h1>The Homey Page</h1> - <p>This page demonstrates <span class="text-danger">nested</span> views.</p> + <p>This page demonstrates <span class="text-danger">nested</span> views.</p> <a ui-sref=".list" class="btn btn-primary">List</a> <a ui-sref=".paragraph" class="btn btn-danger">Paragraph</a> diff --git a/static/partial-fwupdate.html b/static/partial-fwupdate.html new file mode 100644 index 0000000000..829aad8f96 --- /dev/null +++ b/static/partial-fwupdate.html @@ -0,0 +1,17 @@ +<div ng-controller="fwupdateController"> + <div class="container"> + <div class="row"> + <div class="col-lg-8"> + <div class="box box-primary"> + <div class="box-header with-border"> + <h4>Firmware Update + </h4> + </div> + <div class="box-body"> + <input type="file" name="file" onchange="angular.element(this).scope().upload(this.files)" /> + </div> + </div> + </div> + </div> + </div> +</div>
\ No newline at end of file diff --git a/static/partial-fwupdateconfirm.html b/static/partial-fwupdateconfirm.html new file mode 100644 index 0000000000..99ff8dceb7 --- /dev/null +++ b/static/partial-fwupdateconfirm.html @@ -0,0 +1,12 @@ +<div class="modal-header"> + <h2 class="modal-title" id="modal-title">Firmware Update</h2> +</div> +<div class="modal-body" id="modal-body"> + <h3>Loading Version: {{ image_info.major_version }}.{{ image_info.submajor_version }}.{{ image_info.sha1_version }}</h3> + <p>Updating your firmware will cause your BMC to become unresponsive while update is processed. + <p><b> Selected: </b>{{ file_to_load.name }} +</div> +<div class="modal-footer"> + <button class="btn btn-primary" type="button" ng-click="okModal()">OK</button> + <button class="btn btn-warning" type="button" ng-click="dismissModal()">Cancel</button> +</div>
\ No newline at end of file diff --git a/static/partial-login.html b/static/partial-login.html index bee698ec71..4005a0afd0 100644 --- a/static/partial-login.html +++ b/static/partial-login.html @@ -1,18 +1,28 @@ -<div ng-show="logoutreason" class="alert alert-danger">{{logoutreason}}</div> -<form name="form" ng-submit="login()" role="form"> - <div class="form-group"> - <label for="username">Username</label> - <i class="fa fa-key"></i> - <input type="text" name="username" id="username" class="form-control" ng-model="username" required /> - <span ng-show="form.username.$dirty && form.username.$error.required" class="help-block">Username is required</span> +<main class="auth-main"> + <div class="auth-block"> + <h1>Sign in to iBmc</h1> + <div ng-show="logoutreason" class="auth-link">{{logoutreason}}</div> + + <form name="form" ng-submit="login()" role="form" class="form-horizontal"> + <div class="form-group"> + <label for="username" class="col-sm-2 control-label">Username</label> + <div class="col-sm-10"> + <input type="text" name="username" id="username" class="form-control" placeholder="Username" ng-model="username" required /> + </div> + <span ng-show="form.username.$dirty && form.username.$error.required" class="help-block">Username is required</span> + </div> + <div class="form-group"> + <label for="password" class="col-sm-2 control-label">Password</label> + <div class="col-sm-10"> + <input type="password" name="password" id="password" class="form-control" ng-model="password" placeholder="Password" required /> + </div> + <span ng-show="form.password.$dirty && form.password.$error.required" class="help-block">Password is required</span> + </div> + <div class="form-group"> + <div class="col-sm-offset-2 col-sm-10"> + <button type="submit" ng-disabled="form.$invalid || dataLoading" class="btn btn-default btn-auth">Login</button> + </div> + </div> + </form> </div> - <div class="form-group"> - <label for="password">Password</label> - <i class="fa fa-lock"></i> - <input type="password" name="password" id="password" class="form-control" ng-model="password" required /> - <span ng-show="form.password.$dirty && form.password.$error.required" class="help-block">Password is required</span> - </div> - <div class="form-actions"> - <button type="submit" ng-disabled="form.$invalid || dataLoading" class="btn btn-danger">Login</button> - </div> -</form>
\ No newline at end of file +</main>
\ No newline at end of file diff --git a/static/partial-systeminfo.html b/static/partial-systeminfo.html index 13c1e06b31..ceab9d2b7e 100644 --- a/static/partial-systeminfo.html +++ b/static/partial-systeminfo.html @@ -2,8 +2,8 @@ <div class="row"> <div class="col-lg-8"> <div class="box box-primary"> - <div class="box-header with-border"> - <h4>Summary<h4> + <div class="box-header with-border"> + <h4>Summary</h4> </div> <div class="box-body"> <table class="table table-striped system-status-table"> @@ -68,4 +68,4 @@ </div> </div> </div> -</div> +</div>
\ No newline at end of file |