diff options
author | Erik Johnston <erik@matrix.org> | 2014-09-12 18:19:32 +0100 |
---|---|---|
committer | Erik Johnston <erik@matrix.org> | 2014-09-12 18:19:32 +0100 |
commit | e3aec9bc818e203a4883f8863acb96b9c273112a (patch) | |
tree | 762f87365f917f510f00bc6aee7a4068cdd56e76 | |
parent | fix embarassing bug where in-progress messages get vaped when the previous on... (diff) | |
parent | Bump version and change log (diff) | |
download | synapse-e3aec9bc818e203a4883f8863acb96b9c273112a.tar.xz |
Merge branch 'release-v0.2.3' of github.com:matrix-org/synapse
Conflicts: webclient/room/room-controller.js
Diffstat (limited to '')
61 files changed, 8091 insertions, 3887 deletions
diff --git a/CHANGES.rst b/CHANGES.rst index b9b3e9d0ea..ec53438f52 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,22 @@ +Changes in synapse 0.2.3 (2014-09-12) +===================================== + +Homeserver: + * Fix bug where we stopped sending events to remote home servers if a + user from that home server left, even if there were some still in the + room. + * Fix bugs in the state conflict resolution where it was incorrectly + rejecting events. + +Webclient: + * Display room names and topics. + * Allow setting/editing of room names and topics. + * Display information about rooms on the main page. + * Handle ban and kick events in real time. + * VoIP UI and reliabilty improvements. + * Improvements to initial startup speed. + * Don't display duplicate join events. + Changes in synapse 0.2.2 (2014-09-06) ===================================== diff --git a/VERSION b/VERSION index ee1372d33a..7179039691 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2.2 +0.2.3 diff --git a/docs/client-server/howto.rst b/docs/client-server/howto.rst index c02ea8d897..ec941edddd 100644 --- a/docs/client-server/howto.rst +++ b/docs/client-server/howto.rst @@ -24,7 +24,7 @@ If you already have an account, you must **login** into it. `Try out the fiddle`__ -.. __: http://jsfiddle.net/4q2jyxng/ +.. __: http://jsfiddle.net/gh/get/jquery/1.8.3/matrix-org/synapse/tree/master/jsfiddles/register_login Registration ------------ @@ -87,7 +87,7 @@ user and **send a message** to that room. `Try out the fiddle`__ -.. __: http://jsfiddle.net/zL3zto9g/ +.. __: http://jsfiddle.net/gh/get/jquery/1.8.3/matrix-org/synapse/tree/master/jsfiddles/create_room_send_msg Creating a room --------------- @@ -137,7 +137,7 @@ join a room **via a room alias** if one was set up. `Try out the fiddle`__ -.. __: http://jsfiddle.net/7fhotf1b/ +.. __: http://jsfiddle.net/gh/get/jquery/1.8.3/matrix-org/synapse/tree/master/jsfiddles/room_memberships Inviting a user to a room ------------------------- @@ -183,7 +183,7 @@ of getting events, depending on what the client already knows. `Try out the fiddle`__ -.. __: http://jsfiddle.net/vw11mg37/ +.. __: http://jsfiddle.net/gh/get/jquery/1.8.3/matrix-org/synapse/tree/master/jsfiddles/event_stream Getting all state ----------------- @@ -633,4 +633,4 @@ application. `Try out the fiddle`__ -.. __: http://jsfiddle.net/uztL3yme/ +.. __: http://jsfiddle.net/gh/get/jquery/1.8.3/matrix-org/synapse/tree/master/jsfiddles/example_app diff --git a/docs/model/presence.rst b/docs/client-server/model/presence.rst index 7e54505364..7e54505364 100644 --- a/docs/model/presence.rst +++ b/docs/client-server/model/presence.rst diff --git a/docs/model/profiles.rst b/docs/client-server/model/profiles.rst index f7d6bd5679..f7d6bd5679 100644 --- a/docs/model/profiles.rst +++ b/docs/client-server/model/profiles.rst diff --git a/docs/model/protocol_examples.rst b/docs/client-server/model/protocol_examples.rst index 61a599b432..61a599b432 100644 --- a/docs/model/protocol_examples.rst +++ b/docs/client-server/model/protocol_examples.rst diff --git a/docs/model/room-join-workflow.rst b/docs/client-server/model/room-join-workflow.rst index c321a64fab..c321a64fab 100644 --- a/docs/model/room-join-workflow.rst +++ b/docs/client-server/model/room-join-workflow.rst diff --git a/docs/model/rooms.rst b/docs/client-server/model/rooms.rst index 0007e48e30..0007e48e30 100644 --- a/docs/model/rooms.rst +++ b/docs/client-server/model/rooms.rst diff --git a/docs/model/terminology.rst b/docs/client-server/model/terminology.rst index cc6e6760ac..cc6e6760ac 100644 --- a/docs/model/terminology.rst +++ b/docs/client-server/model/terminology.rst diff --git a/docs/model/third-party-id.rst b/docs/client-server/model/third-party-id.rst index 1f8138ddf7..1f8138ddf7 100644 --- a/docs/model/third-party-id.rst +++ b/docs/client-server/model/third-party-id.rst diff --git a/docs/human-id-rules.rst b/docs/human-id-rules.rst new file mode 100644 index 0000000000..3a1ff39892 --- /dev/null +++ b/docs/human-id-rules.rst @@ -0,0 +1,79 @@ +This document outlines the format for human-readable IDs within matrix. + +Overview +-------- +UTF-8 is quickly becoming the standard character encoding set on the web. As +such, Matrix requires that all strings MUST be encoded as UTF-8. However, +using Unicode as the character set for human-readable IDs is troublesome. There +are many different characters which appear identical to each other, but would +identify different users. In addition, there are non-printable characters which +cannot be rendered by the end-user. This opens up a security vulnerability with +phishing/spoofing of IDs, commonly known as a homograph attack. + +Web browers encountered this problem when International Domain Names were +introduced. A variety of checks were put in place in order to protect users. If +an address failed the check, the raw punycode would be displayed to disambiguate +the address. Similar checks are performed by home servers in Matrix. However, +Matrix does not use punycode representations, and so does not show raw punycode +on a failed check. Instead, home servers must outright reject these misleading +IDs. + +Types of human-readable IDs +--------------------------- +There are two main human-readable IDs in question: + +- Room aliases +- User IDs + +Room aliases look like ``#localpart:domain``. These aliases point to opaque +non human-readable room IDs. These pointers can change, so there is already an +issue present with the same ID pointing to a different destination at a later +date. + +User IDs look like ``@localpart:domain``. These represent actual end-users, and +unlike room aliases, there is no layer of indirection. This presents a much +greater concern with homograph attacks. + +Checks +------ +- Similar to web browsers. +- blacklisted chars (e.g. non-printable characters) +- mix of language sets from 'preferred' language not allowed. +- Language sets from CLDR dataset. +- Treated in segments (localpart, domain) +- Additional restrictions for ease of processing IDs. + - Room alias localparts MUST NOT have ``#`` or ``:``. + - User ID localparts MUST NOT have ``@`` or ``:``. + +Rejecting +--------- +- Home servers MUST reject room aliases which do not pass the check, both on + GETs and PUTs. +- Home servers MUST reject user ID localparts which do not pass the check, both + on creation and on events. +- Any home server whose domain does not pass this check, MUST use their punycode + domain name instead of the IDN, to prevent other home servers rejecting you. +- Error code is ``M_FAILED_HUMAN_ID_CHECK``. (generic enough for both failing + due to homograph attacks, and failing due to including ``:`` s, etc) +- Error message MAY go into further information about which characters were + rejected and why. +- Error message SHOULD contain a ``failed_keys`` key which contains an array + of strings which represent the keys which failed the check e.g:: + + failed_keys: [ user_id, room_alias ] + +Other considerations +-------------------- +- Basic security: Informational key on the event attached by HS to say "unsafe + ID". Problem: clients can just ignore it, and since it will appear only very + rarely, easy to forget when implementing clients. +- Moderate security: Requires client handshake. Forces clients to implement + a check, else they cannot communicate with the misleading ID. However, this is + extra overhead in both client implementations and round-trips. +- High security: Outright rejection of the ID at the point of creation / + receiving event. Point of creation rejection is preferable to avoid the ID + entering the system in the first place. However, malicious HSes can just allow + the ID. Hence, other home servers must reject them if they see them in events. + Client never sees the problem ID, provided the HS is correctly implemented. +- High security decided; client doesn't need to worry about it, no additional + protocol complexity aside from rejection of an event. \ No newline at end of file diff --git a/docs/specification.rst b/docs/specification.rst index b15792c00d..aad200c04d 100644 --- a/docs/specification.rst +++ b/docs/specification.rst @@ -1249,7 +1249,33 @@ Or a rejected call: <------- m.call.hangup Calls are negotiated according to the WebRTC specification. - + + +Glare +----- +This specification aims to address the problem of two users calling each other +at roughly the same time and their invites crossing on the wire. It is a far +better experience for the users if their calls are connected if it is clear +that their intention is to set up a call with one another. + +In Matrix, calls are to rooms rather than users (even if those rooms may only +contain one other user) so we consider calls which are to the same room. + +The rules for dealing with such a situation are as follows: + + - If an invite to a room is received whilst the client is preparing to send an + invite to the same room, the client should cancel its outgoing call and + instead automatically accept the incoming call on behalf of the user. + - If an invite to a room is received after the client has sent an invite to the + same room and is waiting for a response, the client should perform a + lexicographical comparison of the call IDs of the two calls and use the + lesser of the two calls, aborting the greater. If the incoming call is the + lesser, the client should accept this call on behalf of the user. + +The call setup should appear seamless to the user as if they had simply placed +a call and the other party had accepted. Thusly, any media stream that had been +setup for use on a call should be transferred and used for the call that +replaces it. Profiles ======== diff --git a/jsfiddles/create_room_send_msg/demo.js b/jsfiddles/create_room_send_msg/demo.js index 3dc7263830..9c346e2f64 100644 --- a/jsfiddles/create_room_send_msg/demo.js +++ b/jsfiddles/create_room_send_msg/demo.js @@ -19,7 +19,12 @@ $('.login').live('click', function() { showLoggedIn(data); }, error: function(err) { - alert(JSON.stringify($.parseJSON(err.responseText))); + var errMsg = "To try this, you need a home server running!"; + var errJson = $.parseJSON(err.responseText); + if (errJson) { + errMsg = JSON.stringify(errJson); + } + alert(errMsg); } }); }); diff --git a/jsfiddles/event_stream/demo.js b/jsfiddles/event_stream/demo.js index 5c81e08caa..acba8391fa 100644 --- a/jsfiddles/event_stream/demo.js +++ b/jsfiddles/event_stream/demo.js @@ -58,7 +58,12 @@ $('.login').live('click', function() { showLoggedIn(data); }, error: function(err) { - alert(JSON.stringify($.parseJSON(err.responseText))); + var errMsg = "To try this, you need a home server running!"; + var errJson = $.parseJSON(err.responseText); + if (errJson) { + errMsg = JSON.stringify(errJson); + } + alert(errMsg); } }); }); diff --git a/jsfiddles/example_app/demo.details b/jsfiddles/example_app/demo.details new file mode 100644 index 0000000000..3f96d3e744 --- /dev/null +++ b/jsfiddles/example_app/demo.details @@ -0,0 +1,7 @@ + name: Example Matrix Client + description: Includes login, live event streaming, creating rooms, sending messages and viewing member lists. + authors: + - matrix.org + resources: + - http://matrix.org + normalize_css: no \ No newline at end of file diff --git a/jsfiddles/register_login/demo.js b/jsfiddles/register_login/demo.js index 9595039173..fffa9e0551 100644 --- a/jsfiddles/register_login/demo.js +++ b/jsfiddles/register_login/demo.js @@ -20,7 +20,12 @@ $('.register').live('click', function() { showLoggedIn(data); }, error: function(err) { - alert(JSON.stringify($.parseJSON(err.responseText))); + var errMsg = "To try this, you need a home server running!"; + var errJson = $.parseJSON(err.responseText); + if (errJson) { + errMsg = JSON.stringify(errJson); + } + alert(errMsg); } }); }); @@ -36,7 +41,12 @@ var login = function(user, password) { showLoggedIn(data); }, error: function(err) { - alert(JSON.stringify($.parseJSON(err.responseText))); + var errMsg = "To try this, you need a home server running!"; + var errJson = $.parseJSON(err.responseText); + if (errJson) { + errMsg = JSON.stringify(errJson); + } + alert(errMsg); } }); }; diff --git a/jsfiddles/room_memberships/demo.js b/jsfiddles/room_memberships/demo.js index 64ba767138..8a7b1aa88e 100644 --- a/jsfiddles/room_memberships/demo.js +++ b/jsfiddles/room_memberships/demo.js @@ -28,7 +28,12 @@ $('.login').live('click', function() { showLoggedIn(data); }, error: function(err) { - alert(JSON.stringify($.parseJSON(err.responseText))); + var errMsg = "To try this, you need a home server running!"; + var errJson = $.parseJSON(err.responseText); + if (errJson) { + errMsg = JSON.stringify(errJson); + } + alert(errMsg); } }); }); diff --git a/synapse/__init__.py b/synapse/__init__.py index 1ed9cdcdf3..d60267ebe4 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -16,4 +16,4 @@ """ This is a reference implementation of a synapse home server. """ -__version__ = "0.2.2" +__version__ = "0.2.3" diff --git a/synapse/federation/units.py b/synapse/federation/units.py index 9740431279..622fe66a8f 100644 --- a/synapse/federation/units.py +++ b/synapse/federation/units.py @@ -69,6 +69,7 @@ class Pdu(JsonEncodedObject): "prev_state_id", "prev_state_origin", "required_power_level", + "user_id", ] internal_keys = [ diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index a0d0f2af16..310cb46fe7 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -593,6 +593,12 @@ class RoomListHandler(BaseHandler): @defer.inlineCallbacks def get_public_room_list(self): chunk = yield self.store.get_rooms(is_public=True) + for room in chunk: + joined_members = yield self.store.get_room_members( + room_id=room["room_id"], + membership=Membership.JOIN + ) + room["num_joined_members"] = len(joined_members) # FIXME (erikj): START is no longer a valid value defer.returnValue({"start": "START", "end": "END", "chunk": chunk}) diff --git a/synapse/state.py b/synapse/state.py index 5dcff27367..9db84c9b5c 100644 --- a/synapse/state.py +++ b/synapse/state.py @@ -115,6 +115,8 @@ class StateHandler(object): is_new = yield self._handle_new_state(new_pdu) + logger.debug("is_new: %s %s %s", is_new, new_pdu.pdu_id, new_pdu.origin) + if is_new: yield self.store.update_current_state( pdu_id=new_pdu.pdu_id, @@ -134,7 +136,9 @@ class StateHandler(object): @defer.inlineCallbacks @log_function def _handle_new_state(self, new_pdu): - tree = yield self.store.get_unresolved_state_tree(new_pdu) + tree, missing_branch = yield self.store.get_unresolved_state_tree( + new_pdu + ) new_branch, current_branch = tree logger.debug( @@ -142,6 +146,28 @@ class StateHandler(object): new_branch, current_branch ) + if missing_branch is not None: + # We're missing some PDUs. Fetch them. + # TODO (erikj): Limit this. + missing_prev = tree[missing_branch][-1] + + pdu_id = missing_prev.prev_state_id + origin = missing_prev.prev_state_origin + + is_missing = yield self.store.get_pdu(pdu_id, origin) is None + if not is_missing: + raise Exception("Conflict resolution failed") + + yield self._replication.get_pdu( + destination=missing_prev.origin, + pdu_origin=origin, + pdu_id=pdu_id, + outlier=True + ) + + updated_current = yield self._handle_new_state(new_pdu) + defer.returnValue(updated_current) + if not current_branch: # There is no current state defer.returnValue(True) @@ -150,84 +176,85 @@ class StateHandler(object): n = new_branch[-1] c = current_branch[-1] - if n.pdu_id == c.pdu_id and n.origin == c.origin: - # We have all the PDUs we need, so we can just do the conflict - # resolution. + common_ancestor = n.pdu_id == c.pdu_id and n.origin == c.origin + + if common_ancestor: + # We found a common ancestor! if len(current_branch) == 1: # This is a direct clobber so we can just... defer.returnValue(True) - conflict_res = [ - self._do_power_level_conflict_res, - self._do_chain_length_conflict_res, - self._do_hash_conflict_res, - ] - - for algo in conflict_res: - new_res, curr_res = algo(new_branch, current_branch) - - if new_res < curr_res: - defer.returnValue(False) - elif new_res > curr_res: - defer.returnValue(True) - - raise Exception("Conflict resolution failed.") - else: - # We need to ask for PDUs. - missing_prev = max( - new_branch[-1], current_branch[-1], - key=lambda x: x.depth - ) + # We didn't find a common ancestor. This is probably fine. + pass - if not hasattr(missing_prev, "prev_state_id"): - # FIXME Hmm - # temporary fallback - for algo in conflict_res: - new_res, curr_res = algo(new_branch, current_branch) + result = yield self._do_conflict_res( + new_branch, current_branch, common_ancestor + ) + defer.returnValue(result) - if new_res < curr_res: - defer.returnValue(False) - elif new_res > curr_res: - defer.returnValue(True) - return + @defer.inlineCallbacks + def _do_conflict_res(self, new_branch, current_branch, common_ancestor): + conflict_res = [ + self._do_power_level_conflict_res, + self._do_chain_length_conflict_res, + self._do_hash_conflict_res, + ] - pdu_id = missing_prev.prev_state_id - origin = missing_prev.prev_state_origin + for algo in conflict_res: + new_res, curr_res = yield defer.maybeDeferred( + algo, + new_branch, current_branch, common_ancestor + ) - is_missing = yield self.store.get_pdu(pdu_id, origin) is None + if new_res < curr_res: + defer.returnValue(False) + elif new_res > curr_res: + defer.returnValue(True) - if not is_missing: - raise Exception("Conflict resolution failed.") + raise Exception("Conflict resolution failed.") - yield self._replication.get_pdu( - destination=missing_prev.origin, - pdu_origin=origin, - pdu_id=pdu_id, - outlier=True - ) - - updated_current = yield self._handle_new_state(new_pdu) - defer.returnValue(updated_current) + @defer.inlineCallbacks + def _do_power_level_conflict_res(self, new_branch, current_branch, + common_ancestor): + new_powers_deferreds = [] + for e in new_branch[:-1] if common_ancestor else new_branch: + if hasattr(e, "user_id"): + new_powers_deferreds.append( + self.store.get_power_level(e.context, e.user_id) + ) + + current_powers_deferreds = [] + for e in current_branch[:-1] if common_ancestor else current_branch: + if hasattr(e, "user_id"): + current_powers_deferreds.append( + self.store.get_power_level(e.context, e.user_id) + ) + + new_powers = yield defer.gatherResults( + new_powers_deferreds, + consumeErrors=True + ) - def _do_power_level_conflict_res(self, new_branch, current_branch): - max_power_new = max( - new_branch[:-1], - key=lambda t: t.power_level - ).power_level + current_powers = yield defer.gatherResults( + current_powers_deferreds, + consumeErrors=True + ) - max_power_current = max( - current_branch[:-1], - key=lambda t: t.power_level - ).power_level + max_power_new = max(new_powers) + max_power_current = max(current_powers) - return (max_power_new, max_power_current) + defer.returnValue( + (max_power_new, max_power_current) + ) - def _do_chain_length_conflict_res(self, new_branch, current_branch): + def _do_chain_length_conflict_res(self, new_branch, current_branch, + common_ancestor): return (len(new_branch), len(current_branch)) - def _do_hash_conflict_res(self, new_branch, current_branch): + def _do_hash_conflict_res(self, new_branch, current_branch, + common_ancestor): new_str = "".join([p.pdu_id + p.origin for p in new_branch]) c_str = "".join([p.pdu_id + p.origin for p in current_branch]) diff --git a/synapse/storage/__init__.py b/synapse/storage/__init__.py index 81c3c94b2e..9201a377b6 100644 --- a/synapse/storage/__init__.py +++ b/synapse/storage/__init__.py @@ -36,7 +36,7 @@ from .registration import RegistrationStore from .room import RoomStore from .roommember import RoomMemberStore from .stream import StreamStore -from .pdu import StatePduStore, PduStore +from .pdu import StatePduStore, PduStore, PdusTable from .transactions import TransactionStore from .keys import KeyStore @@ -47,6 +47,11 @@ import os logger = logging.getLogger(__name__) +class _RollbackButIsFineException(Exception): + """ This exception is used to rollback a transaction without implying + something went wrong. + """ + pass class DataStore(RoomMemberStore, RoomStore, RegistrationStore, StreamStore, ProfileStore, FeedbackStore, @@ -71,14 +76,16 @@ class DataStore(RoomMemberStore, RoomStore, self.min_token -= 1 stream_ordering = self.min_token - latest = yield self._db_pool.runInteraction( - self._persist_pdu_event_txn, - pdu=pdu, - event=event, - backfilled=backfilled, - stream_ordering=stream_ordering, - ) - defer.returnValue(latest) + try: + yield self._db_pool.runInteraction( + self._persist_pdu_event_txn, + pdu=pdu, + event=event, + backfilled=backfilled, + stream_ordering=stream_ordering, + ) + except _RollbackButIsFineException as e: + pass @defer.inlineCallbacks def get_event(self, event_id, allow_none=False): @@ -116,6 +123,12 @@ class DataStore(RoomMemberStore, RoomStore, del cols["content"] del cols["prev_pdus"] cols["content_json"] = json.dumps(pdu.content) + + unrec_keys.update({ + k: v for k, v in cols.items() + if k not in PdusTable.fields + }) + cols["unrecognized_keys"] = json.dumps(unrec_keys) logger.debug("Persisting: %s", repr(cols)) @@ -175,11 +188,12 @@ class DataStore(RoomMemberStore, RoomStore, try: self._simple_insert_txn(txn, "events", vals) except: - logger.exception( + logger.warn( "Failed to persist, probably duplicate: %s", - event.event_id + event.event_id, + exc_info=True, ) - return + raise _RollbackButIsFineException("_persist_event") if not backfilled and hasattr(event, "state_key"): vals = { @@ -205,8 +219,6 @@ class DataStore(RoomMemberStore, RoomStore, } ) - return self._get_room_events_max_id_txn(txn) - @defer.inlineCallbacks def get_current_state(self, room_id, event_type=None, state_key=""): sql = ( diff --git a/synapse/storage/_base.py b/synapse/storage/_base.py index 8037225079..8deaaf93bd 100644 --- a/synapse/storage/_base.py +++ b/synapse/storage/_base.py @@ -17,6 +17,7 @@ import logging from twisted.internet import defer from synapse.api.errors import StoreError +from synapse.util.logutils import log_function import collections import copy @@ -91,6 +92,7 @@ class SQLBaseStore(object): self._simple_insert_txn, table, values, or_replace=or_replace ) + @log_function def _simple_insert_txn(self, txn, table, values, or_replace=False): sql = "%s INTO %s (%s) VALUES(%s)" % ( ("INSERT OR REPLACE" if or_replace else "INSERT"), @@ -98,6 +100,12 @@ class SQLBaseStore(object): ", ".join(k for k in values), ", ".join("?" for k in values) ) + + logger.debug( + "[SQL] %s Args=%s Func=%s", + sql, values.values(), + ) + txn.execute(sql, values.values()) return txn.lastrowid diff --git a/synapse/storage/pdu.py b/synapse/storage/pdu.py index 0bf97e37ee..3c859fdeac 100644 --- a/synapse/storage/pdu.py +++ b/synapse/storage/pdu.py @@ -17,6 +17,7 @@ from twisted.internet import defer from ._base import SQLBaseStore, Table, JoinHelper +from synapse.federation.units import Pdu from synapse.util.logutils import log_function from collections import namedtuple @@ -308,8 +309,8 @@ class PduStore(SQLBaseStore): @defer.inlineCallbacks def get_oldest_pdus_in_context(self, context): - """Get a list of Pdus that we haven't backfilled beyond yet (and haven't - seen). This list is used when we want to backfill backwards and is the + """Get a list of Pdus that we haven't backfilled beyond yet (and havent + seen). This list is used when we want to backfill backwards and is the list we send to the remote server. Args: @@ -516,7 +517,7 @@ class StatePduStore(SQLBaseStore): if not current: logger.debug("get_unresolved_state_tree No current state.") - return return_value + return (return_value, None) return_value.current_branch.append(current) @@ -524,13 +525,16 @@ class StatePduStore(SQLBaseStore): txn, new_pdu, current ) + missing_branch = None for branch, prev_state, state in enum_branches: if state: return_value[branch].append(state) else: + # We don't have prev_state :( + missing_branch = branch break - return return_value + return (return_value, missing_branch) def update_current_state(self, pdu_id, origin, context, pdu_type, state_key): @@ -622,53 +626,6 @@ class StatePduStore(SQLBaseStore): return result - def get_next_missing_pdu(self, new_pdu): - """When we get a new state pdu we need to check whether we need to do - any conflict resolution, if we do then we need to check if we need - to go back and request some more state pdus that we haven't seen yet. - - Args: - txn - new_pdu - - Returns: - PduIdTuple: A pdu that we are missing, or None if we have all the - pdus required to do the conflict resolution. - """ - return self._db_pool.runInteraction( - self._get_next_missing_pdu, new_pdu - ) - - def _get_next_missing_pdu(self, txn, new_pdu): - logger.debug( - "get_next_missing_pdu %s %s", - new_pdu.pdu_id, new_pdu.origin - ) - - current = self._get_current_interaction( - txn, - new_pdu.context, new_pdu.pdu_type, new_pdu.state_key - ) - - if (not current or not current.prev_state_id - or not current.prev_state_origin): - return None - - # Oh look, it's a straight clobber, so wooooo almost no-op. - if (new_pdu.prev_state_id == current.pdu_id - and new_pdu.prev_state_origin == current.origin): - return None - - enum_branches = self._enumerate_state_branches(txn, new_pdu, current) - for branch, prev_state, state in enum_branches: - if not state: - return PduIdTuple( - prev_state.prev_state_id, - prev_state.prev_state_origin - ) - - return None - def handle_new_state(self, new_pdu): """Actually perform conflict resolution on the new_pdu on the assumption we have all the pdus required to perform it. @@ -752,24 +709,11 @@ class StatePduStore(SQLBaseStore): return is_current - @classmethod @log_function - def _enumerate_state_branches(cls, txn, pdu_a, pdu_b): + def _enumerate_state_branches(self, txn, pdu_a, pdu_b): branch_a = pdu_a branch_b = pdu_b - get_query = ( - "SELECT %(fields)s FROM %(pdus)s as p " - "LEFT JOIN %(state)s as s " - "ON p.pdu_id = s.pdu_id AND p.origin = s.origin " - "WHERE p.pdu_id = ? AND p.origin = ? " - ) % { - "fields": _pdu_state_joiner.get_fields( - PdusTable="p", StatePdusTable="s"), - "pdus": PdusTable.table_name, - "state": StatePdusTable.table_name, - } - while True: if (branch_a.pdu_id == branch_b.pdu_id and branch_a.origin == branch_b.origin): @@ -801,13 +745,12 @@ class StatePduStore(SQLBaseStore): branch_a.prev_state_origin ) - logger.debug("getting branch_a prev %s", pdu_tuple) - txn.execute(get_query, pdu_tuple) - prev_branch = branch_a - res = txn.fetchone() - branch_a = PduEntry(*res) if res else None + logger.debug("getting branch_a prev %s", pdu_tuple) + branch_a = self._get_pdu_tuple(txn, *pdu_tuple) + if branch_a: + branch_a = Pdu.from_pdu_tuple(branch_a) logger.debug("branch_a=%s", branch_a) @@ -820,14 +763,13 @@ class StatePduStore(SQLBaseStore): branch_b.prev_state_id, branch_b.prev_state_origin ) - txn.execute(get_query, pdu_tuple) - - logger.debug("getting branch_b prev %s", pdu_tuple) prev_branch = branch_b - res = txn.fetchone() - branch_b = PduEntry(*res) if res else None + logger.debug("getting branch_b prev %s", pdu_tuple) + branch_b = self._get_pdu_tuple(txn, *pdu_tuple) + if branch_b: + branch_b = Pdu.from_pdu_tuple(branch_b) logger.debug("branch_b=%s", branch_b) diff --git a/synapse/storage/roommember.py b/synapse/storage/roommember.py index 9a393e2568..676b2f2653 100644 --- a/synapse/storage/roommember.py +++ b/synapse/storage/roommember.py @@ -18,6 +18,7 @@ from twisted.internet import defer from ._base import SQLBaseStore from synapse.api.constants import Membership +from synapse.util.logutils import log_function import logging @@ -29,8 +30,18 @@ class RoomMemberStore(SQLBaseStore): def _store_room_member_txn(self, txn, event): """Store a room member in the database. """ - target_user_id = event.state_key - domain = self.hs.parse_userid(target_user_id).domain + try: + target_user_id = event.state_key + domain = self.hs.parse_userid(target_user_id).domain + except: + logger.exception("Failed to parse target_user_id=%s", target_user_id) + raise + + logger.debug( + "_store_room_member_txn: target_user_id=%s, membership=%s", + target_user_id, + event.membership, + ) self._simple_insert_txn( txn, @@ -51,12 +62,30 @@ class RoomMemberStore(SQLBaseStore): "VALUES (?, ?)" ) txn.execute(sql, (event.room_id, domain)) - else: - sql = ( - "DELETE FROM room_hosts WHERE room_id = ? AND host = ?" + elif event.membership != Membership.INVITE: + # Check if this was the last person to have left. + member_events = self._get_members_query_txn( + txn, + where_clause="c.room_id = ? AND m.membership = ? AND m.user_id != ?", + where_values=(event.room_id, Membership.JOIN, target_user_id,) ) - txn.execute(sql, (event.room_id, domain)) + joined_domains = set() + for e in member_events: + try: + joined_domains.add( + self.hs.parse_userid(e.state_key).domain + ) + except: + # FIXME: How do we deal with invalid user ids in the db? + logger.exception("Invalid user_id: %s", event.state_key) + + if domain not in joined_domains: + sql = ( + "DELETE FROM room_hosts WHERE room_id = ? AND host = ?" + ) + + txn.execute(sql, (event.room_id, domain)) @defer.inlineCallbacks def get_room_member(self, user_id, room_id): @@ -146,8 +175,13 @@ class RoomMemberStore(SQLBaseStore): vals = where_dict.values() return self._get_members_query(clause, vals) - @defer.inlineCallbacks def _get_members_query(self, where_clause, where_values): + return self._db_pool.runInteraction( + self._get_members_query_txn, + where_clause, where_values + ) + + def _get_members_query_txn(self, txn, where_clause, where_values): sql = ( "SELECT e.* FROM events as e " "INNER JOIN room_memberships as m " @@ -157,12 +191,11 @@ class RoomMemberStore(SQLBaseStore): "WHERE %s " ) % (where_clause,) - rows = yield self._execute_and_decode(sql, *where_values) - - # logger.debug("_get_members_query Got rows %s", rows) + txn.execute(sql, where_values) + rows = self.cursor_to_dict(txn) - results = yield self._parse_events(rows) - defer.returnValue(results) + results = self._parse_events_txn(txn, rows) + return results @defer.inlineCallbacks def user_rooms_intersect(self, user_list): diff --git a/synctl b/synctl index 99dc545ba5..0f83e9cb1f 100755 --- a/synctl +++ b/synctl @@ -4,7 +4,6 @@ SYNAPSE="synapse/app/homeserver.py" CONFIGFILE="homeserver.yaml" PIDFILE="homeserver.pid" -LOGFILE="homeserver.log" GREEN=$'\e[1;32m' NORMAL=$'\e[m' @@ -14,15 +13,13 @@ set -e case "$1" in start) if [ ! -f "$CONFIGFILE" ]; then - echo "No config file found - generating a default one..." - $SYNAPSE -c "$CONFIGFILE" --generate-config - echo "Wrote $CONFIGFILE" - echo "You must now edit this file before continuing" + echo "No config file found" + echo "To generate a config file, run 'python --generate-config'" exit 1 fi echo -n "Starting ..." - $SYNAPSE --daemonize -c "$CONFIGFILE" --pid-file "$PIDFILE" --log-file "$LOGFILE" + $SYNAPSE --daemonize -c "$CONFIGFILE" --pid-file "$PIDFILE" echo "${GREEN}started${NORMAL}" ;; stop) diff --git a/tests/test_state.py b/tests/test_state.py index b01496c40f..16af95b7bc 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -15,21 +15,31 @@ from twisted.internet import defer from twisted.trial import unittest +from twisted.python.log import PythonLoggingObserver from synapse.state import StateHandler from synapse.storage.pdu import PduEntry from synapse.federation.pdu_codec import encode_event_id +from synapse.federation.units import Pdu from collections import namedtuple from mock import Mock +import logging +import mock + ReturnType = namedtuple( "StateReturnType", ["new_branch", "current_branch"] ) +def _gen_get_power_level(power_level_list): + def get_power_level(room_id, user_id): + return defer.succeed(power_level_list.get(user_id, None)) + return get_power_level + class StateTestCase(unittest.TestCase): def setUp(self): self.persistence = Mock(spec=[ @@ -38,6 +48,7 @@ class StateTestCase(unittest.TestCase): "get_latest_pdus_in_context", "get_current_state_pdu", "get_pdu", + "get_power_level", ]) self.replication = Mock(spec=["get_pdu"]) @@ -51,10 +62,12 @@ class StateTestCase(unittest.TestCase): @defer.inlineCallbacks def test_new_state_key(self): # We've never seen anything for this state before - new_pdu = new_fake_pdu_entry("A", "test", "mem", "x", None, 10) + new_pdu = new_fake_pdu("A", "test", "mem", "x", None, "u") + + self.persistence.get_power_level.side_effect = _gen_get_power_level({}) self.persistence.get_unresolved_state_tree.return_value = ( - ReturnType([new_pdu], []) + (ReturnType([new_pdu], []), None) ) is_new = yield self.state.handle_new_state(new_pdu) @@ -74,11 +87,44 @@ class StateTestCase(unittest.TestCase): # We do a direct overwriting of the old state, i.e., the new state # points to the old state. - old_pdu = new_fake_pdu_entry("A", "test", "mem", "x", None, 10) - new_pdu = new_fake_pdu_entry("B", "test", "mem", "x", "A", 5) + old_pdu = new_fake_pdu("A", "test", "mem", "x", None, "u1") + new_pdu = new_fake_pdu("B", "test", "mem", "x", "A", "u2") + + self.persistence.get_power_level.side_effect = _gen_get_power_level({ + "u1": 10, + "u2": 5, + }) self.persistence.get_unresolved_state_tree.return_value = ( - ReturnType([new_pdu, old_pdu], [old_pdu]) + (ReturnType([new_pdu, old_pdu], [old_pdu]), None) + ) + + is_new = yield self.state.handle_new_state(new_pdu) + + self.assertTrue(is_new) + + self.persistence.get_unresolved_state_tree.assert_called_once_with( + new_pdu + ) + + self.assertEqual(1, self.persistence.update_current_state.call_count) + + self.assertFalse(self.replication.get_pdu.called) + + @defer.inlineCallbacks + def test_overwrite(self): + old_pdu_1 = new_fake_pdu("A", "test", "mem", "x", None, "u1") + old_pdu_2 = new_fake_pdu("B", "test", "mem", "x", "A", "u2") + new_pdu = new_fake_pdu("C", "test", "mem", "x", "B", "u3") + + self.persistence.get_power_level.side_effect = _gen_get_power_level({ + "u1": 10, + "u2": 5, + "u3": 0, + }) + + self.persistence.get_unresolved_state_tree.return_value = ( + (ReturnType([new_pdu, old_pdu_2, old_pdu_1], [old_pdu_1]), None) ) is_new = yield self.state.handle_new_state(new_pdu) @@ -98,12 +144,18 @@ class StateTestCase(unittest.TestCase): # We try to update the state based on an outdated state, and have a # too low power level. - old_pdu_1 = new_fake_pdu_entry("A", "test", "mem", "x", None, 10) - old_pdu_2 = new_fake_pdu_entry("B", "test", "mem", "x", None, 10) - new_pdu = new_fake_pdu_entry("C", "test", "mem", "x", "A", 5) + old_pdu_1 = new_fake_pdu("A", "test", "mem", "x", None, "u1") + old_pdu_2 = new_fake_pdu("B", "test", "mem", "x", None, "u2") + new_pdu = new_fake_pdu("C", "test", "mem", "x", "A", "u3") + + self.persistence.get_power_level.side_effect = _gen_get_power_level({ + "u1": 10, + "u2": 10, + "u3": 5, + }) self.persistence.get_unresolved_state_tree.return_value = ( - ReturnType([new_pdu, old_pdu_1], [old_pdu_2, old_pdu_1]) + (ReturnType([new_pdu, old_pdu_1], [old_pdu_2, old_pdu_1]), None) ) is_new = yield self.state.handle_new_state(new_pdu) @@ -123,12 +175,18 @@ class StateTestCase(unittest.TestCase): # We try to update the state based on an outdated state, but have # sufficient power level to force the update. - old_pdu_1 = new_fake_pdu_entry("A", "test", "mem", "x", None, 10) - old_pdu_2 = new_fake_pdu_entry("B", "test", "mem", "x", None, 10) - new_pdu = new_fake_pdu_entry("C", "test", "mem", "x", "A", 15) + old_pdu_1 = new_fake_pdu("A", "test", "mem", "x", None, "u1") + old_pdu_2 = new_fake_pdu("B", "test", "mem", "x", None, "u2") + new_pdu = new_fake_pdu("C", "test", "mem", "x", "A", "u3") + + self.persistence.get_power_level.side_effect = _gen_get_power_level({ + "u1": 10, + "u2": 10, + "u3": 15, + }) self.persistence.get_unresolved_state_tree.return_value = ( - ReturnType([new_pdu, old_pdu_1], [old_pdu_2, old_pdu_1]) + (ReturnType([new_pdu, old_pdu_1], [old_pdu_2, old_pdu_1]), None) ) is_new = yield self.state.handle_new_state(new_pdu) @@ -148,12 +206,18 @@ class StateTestCase(unittest.TestCase): # We try to update the state based on an outdated state, the power # levels are the same and so are the branch lengths - old_pdu_1 = new_fake_pdu_entry("A", "test", "mem", "x", None, 10) - old_pdu_2 = new_fake_pdu_entry("B", "test", "mem", "x", None, 10) - new_pdu = new_fake_pdu_entry("C", "test", "mem", "x", "A", 10) + old_pdu_1 = new_fake_pdu("A", "test", "mem", "x", None, "u1") + old_pdu_2 = new_fake_pdu("B", "test", "mem", "x", None, "u2") + new_pdu = new_fake_pdu("C", "test", "mem", "x", "A", "u3") + + self.persistence.get_power_level.side_effect = _gen_get_power_level({ + "u1": 10, + "u2": 10, + "u3": 10, + }) self.persistence.get_unresolved_state_tree.return_value = ( - ReturnType([new_pdu, old_pdu_1], [old_pdu_2, old_pdu_1]) + (ReturnType([new_pdu, old_pdu_1], [old_pdu_2, old_pdu_1]), None) ) is_new = yield self.state.handle_new_state(new_pdu) @@ -173,13 +237,26 @@ class StateTestCase(unittest.TestCase): # We try to update the state based on an outdated state, the power # levels are the same but the branch length of the new one is longer. - old_pdu_1 = new_fake_pdu_entry("A", "test", "mem", "x", None, 10) - old_pdu_2 = new_fake_pdu_entry("B", "test", "mem", "x", None, 10) - old_pdu_3 = new_fake_pdu_entry("C", "test", "mem", "x", "A", 10) - new_pdu = new_fake_pdu_entry("D", "test", "mem", "x", "C", 10) + old_pdu_1 = new_fake_pdu("A", "test", "mem", "x", None, "u1") + old_pdu_2 = new_fake_pdu("B", "test", "mem", "x", None, "u2") + old_pdu_3 = new_fake_pdu("C", "test", "mem", "x", "A", "u3") + new_pdu = new_fake_pdu("D", "test", "mem", "x", "C", "u4") + + self.persistence.get_power_level.side_effect = _gen_get_power_level({ + "u1": 10, + "u2": 10, + "u3": 10, + "u4": 10, + }) self.persistence.get_unresolved_state_tree.return_value = ( - ReturnType([new_pdu, old_pdu_3, old_pdu_1], [old_pdu_2, old_pdu_1]) + ( + ReturnType( + [new_pdu, old_pdu_3, old_pdu_1], + [old_pdu_2, old_pdu_1] + ), + None + ) ) is_new = yield self.state.handle_new_state(new_pdu) @@ -200,22 +277,38 @@ class StateTestCase(unittest.TestCase): # triggering a get_pdu request # The pdu we haven't seen - old_pdu_1 = new_fake_pdu_entry("A", "test", "mem", "x", None, 10) + old_pdu_1 = new_fake_pdu( + "A", "test", "mem", "x", None, "u1", depth=0 + ) + + old_pdu_2 = new_fake_pdu( + "B", "test", "mem", "x", "A", "u2", depth=1 + ) + new_pdu = new_fake_pdu( + "C", "test", "mem", "x", "A", "u3", depth=2 + ) - old_pdu_2 = new_fake_pdu_entry("B", "test", "mem", "x", None, 10) - new_pdu = new_fake_pdu_entry("C", "test", "mem", "x", "A", 20) + self.persistence.get_power_level.side_effect = _gen_get_power_level({ + "u1": 10, + "u2": 10, + "u3": 20, + }) # The return_value of `get_unresolved_state_tree`, which changes after # the call to get_pdu - tree_to_return = [ReturnType([new_pdu], [old_pdu_2])] + tree_to_return = [(ReturnType([new_pdu], [old_pdu_2]), 0)] def return_tree(p): return tree_to_return[0] - def set_return_tree(*args, **kwargs): - tree_to_return[0] = ReturnType( - [new_pdu, old_pdu_1], [old_pdu_2, old_pdu_1] + def set_return_tree(destination, pdu_origin, pdu_id, outlier=False): + tree_to_return[0] = ( + ReturnType( + [new_pdu, old_pdu_1], [old_pdu_2, old_pdu_1] + ), + None ) + return defer.succeed(None) self.persistence.get_unresolved_state_tree.side_effect = return_tree @@ -227,6 +320,13 @@ class StateTestCase(unittest.TestCase): self.assertTrue(is_new) + self.replication.get_pdu.assert_called_with( + destination=new_pdu.origin, + pdu_origin=old_pdu_1.origin, + pdu_id=old_pdu_1.pdu_id, + outlier=True + ) + self.persistence.get_unresolved_state_tree.assert_called_with( new_pdu ) @@ -238,11 +338,232 @@ class StateTestCase(unittest.TestCase): self.assertEqual(1, self.persistence.update_current_state.call_count) @defer.inlineCallbacks + def test_missing_pdu_depth_1(self): + # We try to update state against a PDU we haven't yet seen, + # triggering a get_pdu request + + # The pdu we haven't seen + old_pdu_1 = new_fake_pdu( + "A", "test", "mem", "x", None, "u1", depth=0 + ) + + old_pdu_2 = new_fake_pdu( + "B", "test", "mem", "x", "A", "u2", depth=2 + ) + old_pdu_3 = new_fake_pdu( + "C", "test", "mem", "x", "B", "u3", depth=3 + ) + new_pdu = new_fake_pdu( + "D", "test", "mem", "x", "A", "u4", depth=4 + ) + + self.persistence.get_power_level.side_effect = _gen_get_power_level({ + "u1": 10, + "u2": 10, + "u3": 10, + "u4": 20, + }) + + # The return_value of `get_unresolved_state_tree`, which changes after + # the call to get_pdu + tree_to_return = [ + ( + ReturnType([new_pdu], [old_pdu_3]), + 0 + ), + ( + ReturnType( + [new_pdu, old_pdu_1], [old_pdu_3] + ), + 1 + ), + ( + ReturnType( + [new_pdu, old_pdu_1], [old_pdu_3, old_pdu_2, old_pdu_1] + ), + None + ), + ] + + to_return = [0] + + def return_tree(p): + return tree_to_return[to_return[0]] + + def set_return_tree(destination, pdu_origin, pdu_id, outlier=False): + to_return[0] += 1 + return defer.succeed(None) + + self.persistence.get_unresolved_state_tree.side_effect = return_tree + + self.replication.get_pdu.side_effect = set_return_tree + + self.persistence.get_pdu.return_value = None + + is_new = yield self.state.handle_new_state(new_pdu) + + self.assertTrue(is_new) + + self.assertEqual(2, self.replication.get_pdu.call_count) + + self.replication.get_pdu.assert_has_calls( + [ + mock.call( + destination=new_pdu.origin, + pdu_origin=old_pdu_1.origin, + pdu_id=old_pdu_1.pdu_id, + outlier=True + ), + mock.call( + destination=old_pdu_3.origin, + pdu_origin=old_pdu_2.origin, + pdu_id=old_pdu_2.pdu_id, + outlier=True + ), + ] + ) + + self.persistence.get_unresolved_state_tree.assert_called_with( + new_pdu + ) + + self.assertEquals( + 3, self.persistence.get_unresolved_state_tree.call_count + ) + + self.assertEqual(1, self.persistence.update_current_state.call_count) + + @defer.inlineCallbacks + def test_missing_pdu_depth_2(self): + # We try to update state against a PDU we haven't yet seen, + # triggering a get_pdu request + + # The pdu we haven't seen + old_pdu_1 = new_fake_pdu( + "A", "test", "mem", "x", None, "u1", depth=0 + ) + + old_pdu_2 = new_fake_pdu( + "B", "test", "mem", "x", "A", "u2", depth=2 + ) + old_pdu_3 = new_fake_pdu( + "C", "test", "mem", "x", "B", "u3", depth=3 + ) + new_pdu = new_fake_pdu( + "D", "test", "mem", "x", "A", "u4", depth=1 + ) + + self.persistence.get_power_level.side_effect = _gen_get_power_level({ + "u1": 10, + "u2": 10, + "u3": 10, + "u4": 20, + }) + + # The return_value of `get_unresolved_state_tree`, which changes after + # the call to get_pdu + tree_to_return = [ + ( + ReturnType([new_pdu], [old_pdu_3]), + 1, + ), + ( + ReturnType( + [new_pdu], [old_pdu_3, old_pdu_2] + ), + 0, + ), + ( + ReturnType( + [new_pdu, old_pdu_1], [old_pdu_3, old_pdu_2, old_pdu_1] + ), + None + ), + ] + + to_return = [0] + + def return_tree(p): + return tree_to_return[to_return[0]] + + def set_return_tree(destination, pdu_origin, pdu_id, outlier=False): + to_return[0] += 1 + return defer.succeed(None) + + self.persistence.get_unresolved_state_tree.side_effect = return_tree + + self.replication.get_pdu.side_effect = set_return_tree + + self.persistence.get_pdu.return_value = None + + is_new = yield self.state.handle_new_state(new_pdu) + + self.assertTrue(is_new) + + self.assertEqual(2, self.replication.get_pdu.call_count) + + self.replication.get_pdu.assert_has_calls( + [ + mock.call( + destination=old_pdu_3.origin, + pdu_origin=old_pdu_2.origin, + pdu_id=old_pdu_2.pdu_id, + outlier=True + ), + mock.call( + destination=new_pdu.origin, + pdu_origin=old_pdu_1.origin, + pdu_id=old_pdu_1.pdu_id, + outlier=True + ), + ] + ) + + self.persistence.get_unresolved_state_tree.assert_called_with( + new_pdu + ) + + self.assertEquals( + 3, self.persistence.get_unresolved_state_tree.call_count + ) + + self.assertEqual(1, self.persistence.update_current_state.call_count) + + @defer.inlineCallbacks + def test_no_common_ancestor(self): + # We do a direct overwriting of the old state, i.e., the new state + # points to the old state. + + old_pdu = new_fake_pdu("A", "test", "mem", "x", None, "u1") + new_pdu = new_fake_pdu("B", "test", "mem", "x", None, "u2") + + self.persistence.get_power_level.side_effect = _gen_get_power_level({ + "u1": 5, + "u2": 10, + }) + + self.persistence.get_unresolved_state_tree.return_value = ( + (ReturnType([new_pdu], [old_pdu]), None) + ) + + is_new = yield self.state.handle_new_state(new_pdu) + + self.assertTrue(is_new) + + self.persistence.get_unresolved_state_tree.assert_called_once_with( + new_pdu + ) + + self.assertEqual(1, self.persistence.update_current_state.call_count) + + self.assertFalse(self.replication.get_pdu.called) + + @defer.inlineCallbacks def test_new_event(self): event = Mock() event.event_id = "12123123@test" - state_pdu = new_fake_pdu_entry("C", "test", "mem", "x", "A", 20) + state_pdu = new_fake_pdu("C", "test", "mem", "x", "A", 20) snapshot = Mock() snapshot.prev_state_pdu = state_pdu @@ -269,24 +590,25 @@ class StateTestCase(unittest.TestCase): ) -def new_fake_pdu_entry(pdu_id, context, pdu_type, state_key, prev_state_id, - power_level): - new_pdu = PduEntry( +def new_fake_pdu(pdu_id, context, pdu_type, state_key, prev_state_id, + user_id, depth=0): + new_pdu = Pdu( pdu_id=pdu_id, pdu_type=pdu_type, state_key=state_key, - power_level=power_level, + user_id=user_id, prev_state_id=prev_state_id, origin="example.com", context="context", ts=1405353060021, - depth=0, + depth=depth, content_json="{}", unrecognized_keys="{}", outlier=True, is_state=True, prev_state_origin="example.com", have_processed=True, + content={}, ) return new_pdu diff --git a/webclient/app-controller.js b/webclient/app-controller.js index 064bde3ab2..6c3759878b 100644 --- a/webclient/app-controller.js +++ b/webclient/app-controller.js @@ -21,8 +21,8 @@ limitations under the License. 'use strict'; angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'eventStreamService']) -.controller('MatrixWebClientController', ['$scope', '$location', '$rootScope', '$timeout', '$animate', 'matrixService', 'mPresence', 'eventStreamService', 'matrixPhoneService', - function($scope, $location, $rootScope, $timeout, $animate, matrixService, mPresence, eventStreamService, matrixPhoneService) { +.controller('MatrixWebClientController', ['$scope', '$location', '$rootScope', '$timeout', '$animate', 'matrixService', 'mPresence', 'eventStreamService', 'eventHandlerService', 'matrixPhoneService', + function($scope, $location, $rootScope, $timeout, $animate, matrixService, mPresence, eventStreamService, eventHandlerService, matrixPhoneService) { // Check current URL to avoid to display the logout button on the login page $scope.location = $location.path(); @@ -73,7 +73,10 @@ angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'even // Clean permanent data matrixService.setConfig({}); matrixService.saveConfig(); - + + // Reset cached data + eventHandlerService.reset(); + // And go to the login page $location.url("login"); }; @@ -96,50 +99,80 @@ angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'even delete roomMembers[matrixService.config().user_id]; $rootScope.currentCall.user_id = Object.keys(roomMembers)[0]; + + // set it to the user ID until we fetch the display name + $rootScope.currentCall.userProfile = { displayname: $rootScope.currentCall.user_id }; + matrixService.getProfile($rootScope.currentCall.user_id).then( function(response) { - $rootScope.currentCall.userProfile = response.data; + if (response.data.displayname) $rootScope.currentCall.userProfile.displayname = response.data.displayname; + if (response.data.avatar_url) $rootScope.currentCall.userProfile.avatar_url = response.data.avatar_url; }, function(error) { $scope.feedback = "Can't load user profile"; } ); }); + $rootScope.$watch('currentCall.state', function(newVal, oldVal) { + if (newVal == 'ringing') { + angular.element('#ringbackAudio')[0].pause(); + angular.element('#ringAudio')[0].load(); + angular.element('#ringAudio')[0].play(); + } else if (newVal == 'invite_sent') { + angular.element('#ringAudio')[0].pause(); + angular.element('#ringbackAudio')[0].load(); + angular.element('#ringbackAudio')[0].play(); + } else if (newVal == 'ended' && oldVal == 'connected') { + angular.element('#ringAudio')[0].pause(); + angular.element('#ringbackAudio')[0].pause(); + angular.element('#callendAudio')[0].play(); + } else if (newVal == 'ended' && oldVal == 'invite_sent' && $rootScope.currentCall.hangupParty == 'remote') { + angular.element('#ringAudio')[0].pause(); + angular.element('#ringbackAudio')[0].pause(); + angular.element('#busyAudio')[0].play(); + } else if (oldVal == 'invite_sent') { + angular.element('#ringbackAudio')[0].pause(); + } else if (oldVal == 'ringing') { + angular.element('#ringAudio')[0].pause(); + } + }); $rootScope.$on(matrixPhoneService.INCOMING_CALL_EVENT, function(ngEvent, call) { - console.trace("incoming call"); + console.log("incoming call"); + if ($rootScope.currentCall && $rootScope.currentCall.state != 'ended') { + console.log("rejecting call because we're already in a call"); + call.hangup(); + return; + } call.onError = $scope.onCallError; call.onHangup = $scope.onCallHangup; $rootScope.currentCall = call; }); + $rootScope.$on(matrixPhoneService.REPLACED_CALL_EVENT, function(ngEvent, oldCall, newCall) { + console.log("call ID "+oldCall.call_id+" has been replaced by call ID "+newCall.call_id+"!"); + newCall.onError = $scope.onCallError; + newCall.onHangup = $scope.onCallHangup; + $rootScope.currentCall = newCall; + }); + $scope.answerCall = function() { $rootScope.currentCall.answer(); }; $scope.hangupCall = function() { $rootScope.currentCall.hangup(); - - $timeout(function() { - var icon = angular.element('#callEndedIcon'); - $animate.addClass(icon, 'callIconRotate'); - $timeout(function(){ - $rootScope.currentCall = undefined; - }, 2000); - }, 100); }; $rootScope.onCallError = function(errStr) { $scope.feedback = errStr; } - $rootScope.onCallHangup = function() { - $timeout(function() { - var icon = angular.element('#callEndedIcon'); - $animate.addClass(icon, 'callIconRotate'); + $rootScope.onCallHangup = function(call) { + if (call == $rootScope.currentCall) { $timeout(function(){ - $rootScope.currentCall = undefined; - }, 2000); - }, 100); + if (call == $rootScope.currentCall) $rootScope.currentCall = undefined; + }, 4070); + } } }]); diff --git a/webclient/app.css b/webclient/app.css index 7698cb4fda..a277bd2a59 100755 --- a/webclient/app.css +++ b/webclient/app.css @@ -55,20 +55,20 @@ a:active { color: #000; } margin-left: 4px; margin-right: 4px; margin-top: 8px; - -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; - -moz-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; - -o-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; - transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; } -.callIconRotate { - -webkit-transform: rotateZ(45deg); - -moz-transform: rotateZ(45deg); - -ms-transform: rotateZ(45deg); - -o-transform: rotateZ(45deg); +#callEndedIcon { + transition:all linear 0.5s; +} + +#callEndedIcon { transform: rotateZ(45deg); } +#callEndedIcon.ng-hide { + transform: rotateZ(0deg); +} + #callPeerImage { width: 32px; height: 32px; @@ -220,12 +220,6 @@ a:active { color: #000; } height: 100%; } -#roomName { - float: right; - font-size: 16px; - margin-top: 15px; -} - #roomHeader { margin: auto; padding-left: 20px; @@ -263,12 +257,80 @@ a:active { color: #000; } #mainInput { width: 100%; + resize: none; } .blink { background-color: #faa; } +.roomHighlight { + font-weight: bold; +} + +.publicTable { + max-width: 480px; + width: 100%; + border-collapse: collapse; +} +.publicTable tr { + width: 100%; +} +.publicTable td { + vertical-align: text-top; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.publicRoomEntry { + max-width: 430px; +} + +.publicRoomJoinedUsers { + width: 5em; + text-align: right; + font-size: 12px; + color: #888; +} + +.publicRoomTopic { + color: #888; + font-size: 12px; + overflow: hidden; + padding-bottom: 5px; + border-bottom: 1px #ddd solid; +} + +#roomName { + font-size: 16px; + text-align: right; +} + +#roomTopic { + font-size: 13px; + text-align: right; +} + +.roomNameInput, .roomTopicInput { + width: 100%; +} + +.roomNameSection, .roomTopicSection { + float: right; + width: 100%; +} + +.roomNameSetNew, .roomTopicSetNew { + float: right; +} + +.roomHeaderInfo { + float: right; + margin-top: 15px; + width: 50%; +} + /*** Participant list ***/ #usersTableWrapper { @@ -465,6 +527,10 @@ a:active { color: #000; } text-align: left ! important; } +.bubble .message { + /* Break lines when encountering CR+LF */ + white-space: pre; +} .bubble .messagePending { opacity: 0.3 } diff --git a/webclient/app.js b/webclient/app.js index d25e2a6234..9370f773b3 100644 --- a/webclient/app.js +++ b/webclient/app.js @@ -16,6 +16,7 @@ limitations under the License. var matrixWebClient = angular.module('matrixWebClient', [ 'ngRoute', + 'ngAnimate', 'MatrixWebClientController', 'LoginController', 'RegisterController', diff --git a/webclient/components/matrix/event-handler-service.js b/webclient/components/matrix/event-handler-service.js index d2bb31053f..705a5a07f2 100644 --- a/webclient/components/matrix/event-handler-service.js +++ b/webclient/components/matrix/event-handler-service.js @@ -35,13 +35,20 @@ angular.module('eventHandlerService', []) var POWERLEVEL_EVENT = "POWERLEVEL_EVENT"; var CALL_EVENT = "CALL_EVENT"; var NAME_EVENT = "NAME_EVENT"; + var TOPIC_EVENT = "TOPIC_EVENT"; + var RESET_EVENT = "RESET_EVENT"; // eventHandlerService has been resetted + + var initialSyncDeferred; + + var reset = function() { + initialSyncDeferred = $q.defer(); + + $rootScope.events = { + rooms: {} // will contain roomId: { messages:[], members:{userid1: event} } + }; + } + reset(); - var InitialSyncDeferred = $q.defer(); - - $rootScope.events = { - rooms: {} // will contain roomId: { messages:[], members:{userid1: event} } - }; - // used for dedupping events - could be expanded in future... // FIXME: means that we leak memory over time (along with lots of the rest // of the app, given we never try to reap memory yet) @@ -55,6 +62,11 @@ angular.module('eventHandlerService', []) $rootScope.events.rooms[room_id] = {}; $rootScope.events.rooms[room_id].messages = []; $rootScope.events.rooms[room_id].members = {}; + + // Pagination information + $rootScope.events.rooms[room_id].pagination = { + earliest_token: "END" // how far back we've paginated + }; } }; @@ -64,9 +76,37 @@ angular.module('eventHandlerService', []) } }; - var handleRoomCreate = function(event, isLiveEvent) { - initRoom(event.room_id); + // Generic method to handle events data + var handleRoomDateEvent = function(event, isLiveEvent, addToRoomMessages) { + // Add topic changes as if they were a room message + if (addToRoomMessages) { + if (isLiveEvent) { + $rootScope.events.rooms[event.room_id].messages.push(event); + } + else { + $rootScope.events.rooms[event.room_id].messages.unshift(event); + } + } + // live events always update, but non-live events only update if the + // ts is later. + var latestData = true; + if (!isLiveEvent) { + var eventTs = event.ts; + var storedEvent = $rootScope.events.rooms[event.room_id][event.type]; + if (storedEvent) { + if (storedEvent.ts > eventTs) { + // ignore it, we have a newer one already. + latestData = false; + } + } + } + if (latestData) { + $rootScope.events.rooms[event.room_id][event.type] = event; + } + }; + + var handleRoomCreate = function(event, isLiveEvent) { // For now, we do not use the event data. Simply signal it to the app controllers $rootScope.$broadcast(ROOM_CREATE_EVENT, event, isLiveEvent); }; @@ -76,13 +116,18 @@ angular.module('eventHandlerService', []) }; var handleMessage = function(event, isLiveEvent) { - initRoom(event.room_id); - if (isLiveEvent) { if (event.user_id === matrixService.config().user_id && (event.content.msgtype === "m.text" || event.content.msgtype === "m.emote") ) { - // assume we've already echoed it - // FIXME: track events by ID and ungrey the right message to show it's been delivered + // Assume we've already echoed it. So, there is a fake event in the messages list of the room + // Replace this fake event by the true one + var index = getRoomEventIndex(event.room_id, event.event_id); + if (index) { + $rootScope.events.rooms[event.room_id].messages[index] = event; + } + else { + $rootScope.events.rooms[event.room_id].messages.push(event); + } } else { $rootScope.events.rooms[event.room_id].messages.push(event); @@ -100,9 +145,7 @@ angular.module('eventHandlerService', []) $rootScope.$broadcast(MSG_EVENT, event, isLiveEvent); }; - var handleRoomMember = function(event, isLiveEvent) { - initRoom(event.room_id); - + var handleRoomMember = function(event, isLiveEvent, isStateEvent) { // if the server is stupidly re-relaying a no-op join, discard it. if (event.prev_content && event.content.membership === "join" && @@ -112,7 +155,10 @@ angular.module('eventHandlerService', []) } // add membership changes as if they were a room message if something interesting changed - if (event.content.prev !== event.content.membership) { + // Exception: Do not do this if the event is a room state event because such events already come + // as room messages events. Moreover, when they come as room messages events, they are relatively ordered + // with other other room messages + if (event.content.prev !== event.content.membership && !isStateEvent) { if (isLiveEvent) { $rootScope.events.rooms[event.room_id].messages.push(event); } @@ -121,8 +167,13 @@ angular.module('eventHandlerService', []) } } - $rootScope.events.rooms[event.room_id].members[event.state_key] = event; - $rootScope.$broadcast(MEMBER_EVENT, event, isLiveEvent); + // Use data from state event or the latest data from the stream. + // Do not care of events that come when paginating back + if (isStateEvent || isLiveEvent) { + $rootScope.events.rooms[event.room_id].members[event.state_key] = event; + } + + $rootScope.$broadcast(MEMBER_EVENT, event, isLiveEvent, isStateEvent); }; var handlePresence = function(event, isLiveEvent) { @@ -131,8 +182,6 @@ angular.module('eventHandlerService', []) }; var handlePowerLevels = function(event, isLiveEvent) { - initRoom(event.room_id); - // Keep the latest data. Do not care of events that come when paginating back if (!$rootScope.events.rooms[event.room_id][event.type] || isLiveEvent) { $rootScope.events.rooms[event.room_id][event.type] = event; @@ -140,19 +189,50 @@ angular.module('eventHandlerService', []) } }; - var handleRoomName = function(event, isLiveEvent) { - console.log("handleRoomName " + isLiveEvent); - - initRoom(event.room_id); - - $rootScope.events.rooms[event.room_id][event.type] = event; + var handleRoomName = function(event, isLiveEvent, isStateEvent) { + console.log("handleRoomName room_id: " + event.room_id + " - isLiveEvent: " + isLiveEvent + " - name: " + event.content.name); + handleRoomDateEvent(event, isLiveEvent, !isStateEvent); $rootScope.$broadcast(NAME_EVENT, event, isLiveEvent); }; + + + var handleRoomTopic = function(event, isLiveEvent, isStateEvent) { + console.log("handleRoomTopic room_id: " + event.room_id + " - isLiveEvent: " + isLiveEvent + " - topic: " + event.content.topic); + handleRoomDateEvent(event, isLiveEvent, !isStateEvent); + $rootScope.$broadcast(TOPIC_EVENT, event, isLiveEvent); + }; var handleCallEvent = function(event, isLiveEvent) { $rootScope.$broadcast(CALL_EVENT, event, isLiveEvent); + if (event.type == 'm.call.invite') { + $rootScope.events.rooms[event.room_id].messages.push(event); + } }; + /** + * Get the index of the event in $rootScope.events.rooms[room_id].messages + * @param {type} room_id the room id + * @param {type} event_id the event id to look for + * @returns {Number | undefined} the index. undefined if not found. + */ + var getRoomEventIndex = function(room_id, event_id) { + var index; + + var room = $rootScope.events.rooms[room_id]; + if (room) { + // Start looking from the tail since the first goal of this function + // is to find a messaged among the latest ones + for (var i = room.messages.length - 1; i > 0; i--) { + var message = room.messages[i]; + if (event_id === message.event_id) { + index = i; + break; + } + } + } + return index; + } + return { ROOM_CREATE_EVENT: ROOM_CREATE_EVENT, MSG_EVENT: MSG_EVENT, @@ -161,19 +241,37 @@ angular.module('eventHandlerService', []) POWERLEVEL_EVENT: POWERLEVEL_EVENT, CALL_EVENT: CALL_EVENT, NAME_EVENT: NAME_EVENT, + TOPIC_EVENT: TOPIC_EVENT, + RESET_EVENT: RESET_EVENT, + + reset: function() { + reset(); + $rootScope.$broadcast(RESET_EVENT); + }, - handleEvent: function(event, isLiveEvent) { - // FIXME: event duplication suppression is all broken as the code currently expect to handles - // events multiple times to get their side-effects... -/* - if (eventMap[event.event_id]) { - console.log("discarding duplicate event: " + JSON.stringify(event)); - return; - } - else { - eventMap[event.event_id] = 1; + handleEvent: function(event, isLiveEvent, isStateEvent) { + + // FIXME: /initialSync on a particular room is not yet available + // So initRoom on a new room is not called. Make sure the room data is initialised here + initRoom(event.room_id); + + // Avoid duplicated events + // Needed for rooms where initialSync has not been done. + // In this case, we do not know where to start pagination. So, it starts from the END + // and we can have the same event (ex: joined, invitation) coming from the pagination + // AND from the event stream. + // FIXME: This workaround should be no more required when /initialSync on a particular room + // will be available (as opposite to the global /initialSync done at startup) + if (!isStateEvent) { // Do not consider state events + if (event.event_id && eventMap[event.event_id]) { + console.log("discarding duplicate event: " + JSON.stringify(event, undefined, 4)); + return; + } + else { + eventMap[event.event_id] = 1; + } } -*/ + if (event.type.indexOf('m.call.') === 0) { handleCallEvent(event, isLiveEvent); } @@ -189,7 +287,7 @@ angular.module('eventHandlerService', []) handleMessage(event, isLiveEvent); break; case "m.room.member": - handleRoomMember(event, isLiveEvent); + handleRoomMember(event, isLiveEvent, isStateEvent); break; case "m.presence": handlePresence(event, isLiveEvent); @@ -202,7 +300,10 @@ angular.module('eventHandlerService', []) handlePowerLevels(event, isLiveEvent); break; case 'm.room.name': - handleRoomName(event, isLiveEvent); + handleRoomName(event, isLiveEvent, isStateEvent); + break; + case 'm.room.topic': + handleRoomTopic(event, isLiveEvent, isStateEvent); break; default: console.log("Unable to handle event type " + event.type); @@ -214,20 +315,30 @@ angular.module('eventHandlerService', []) // isLiveEvents determines whether notifications should be shown, whether // messages get appended to the start/end of lists, etc. - handleEvents: function(events, isLiveEvents) { + handleEvents: function(events, isLiveEvents, isStateEvents) { for (var i=0; i<events.length; i++) { - this.handleEvent(events[i], isLiveEvents); + this.handleEvent(events[i], isLiveEvents, isStateEvents); } }, - handleInitialSyncDone: function() { + // Handle messages from /initialSync or /messages + handleRoomMessages: function(room_id, messages, isLiveEvents) { + initRoom(room_id); + this.handleEvents(messages.chunk, isLiveEvents); + + // Store how far back we've paginated + // This assumes the paginations requests are contiguous and in reverse chronological order + $rootScope.events.rooms[room_id].pagination.earliest_token = messages.end; + }, + + handleInitialSyncDone: function(initialSyncData) { console.log("# handleInitialSyncDone"); - InitialSyncDeferred.resolve($rootScope.events, $rootScope.presence); + initialSyncDeferred.resolve(initialSyncData); }, // Returns a promise that resolves when the initialSync request has been processed waitForInitialSyncCompletion: function() { - return InitialSyncDeferred.promise; + return initialSyncDeferred.promise; }, resetRoomMessages: function(room_id) { diff --git a/webclient/components/matrix/event-stream-service.js b/webclient/components/matrix/event-stream-service.js index ed4f3b2ffc..03b805213d 100644 --- a/webclient/components/matrix/event-stream-service.js +++ b/webclient/components/matrix/event-stream-service.js @@ -104,15 +104,19 @@ angular.module('eventStreamService', []) settings.isActive = true; var deferred = $q.defer(); - // FIXME: We are discarding all the messages. - matrixService.rooms().then( + // Initial sync: get all information and the last message of all rooms of the user + matrixService.initialSync(1, false).then( function(response) { var rooms = response.data.rooms; for (var i = 0; i < rooms.length; ++i) { var room = rooms[i]; - // console.log("got room: " + room.room_id); + + if ("messages" in room) { + eventHandlerService.handleRoomMessages(room.room_id, room.messages, false); + } + if ("state" in room) { - eventHandlerService.handleEvents(room.state, false); + eventHandlerService.handleEvents(room.state, false, true); } } @@ -120,8 +124,9 @@ angular.module('eventStreamService', []) eventHandlerService.handleEvents(presence, false); // Initial sync is done - eventHandlerService.handleInitialSyncDone(); + eventHandlerService.handleInitialSyncDone(response); + // Start event streaming from that point settings.from = response.data.end; doEventStream(deferred); }, diff --git a/webclient/components/matrix/matrix-call.js b/webclient/components/matrix/matrix-call.js index 3cb5e8b693..2e3e2b0967 100644 --- a/webclient/components/matrix/matrix-call.js +++ b/webclient/components/matrix/matrix-call.js @@ -35,8 +35,13 @@ var forAllTracksOnStream = function(s, f) { forAllAudioTracksOnStream(s, f); } +navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia; +window.RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection; // but not mozRTCPeerConnection because its interface is not compatible +window.RTCSessionDescription = window.RTCSessionDescription || window.webkitRTCSessionDescription || window.mozRTCSessionDescription; +window.RTCIceCandidate = window.RTCIceCandidate || window.webkitRTCIceCandidate || window.mozRTCIceCandidate; + angular.module('MatrixCall', []) -.factory('MatrixCall', ['matrixService', 'matrixPhoneService', '$rootScope', function MatrixCallFactory(matrixService, matrixPhoneService, $rootScope) { +.factory('MatrixCall', ['matrixService', 'matrixPhoneService', '$rootScope', '$timeout', function MatrixCallFactory(matrixService, matrixPhoneService, $rootScope, $timeout) { var MatrixCall = function(room_id) { this.room_id = room_id; this.call_id = "c" + new Date().getTime(); @@ -44,93 +49,117 @@ angular.module('MatrixCall', []) this.didConnect = false; } - navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia; - - window.RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection; + MatrixCall.prototype.createPeerConnection = function() { + var stunServer = 'stun:stun.l.google.com:19302'; + var pc; + if (window.mozRTCPeerConnection) { + pc = window.mozRTCPeerConnection({'url': stunServer}); + } else { + pc = new window.RTCPeerConnection({"iceServers":[{"urls":"stun:stun.l.google.com:19302"}]}); + } + var self = this; + pc.oniceconnectionstatechange = function() { self.onIceConnectionStateChanged(); }; + pc.onsignalingstatechange = function() { self.onSignallingStateChanged(); }; + pc.onicecandidate = function(c) { self.gotLocalIceCandidate(c); }; + pc.onaddstream = function(s) { self.onAddStream(s); }; + return pc; + } - MatrixCall.prototype.placeCall = function() { - self = this; + MatrixCall.prototype.placeCall = function(config) { + var self = this; matrixPhoneService.callPlaced(this); - navigator.getUserMedia({audio: true, video: false}, function(s) { self.gotUserMediaForInvite(s); }, function(e) { self.getUserMediaFailed(e); }); - self.state = 'wait_local_media'; + navigator.getUserMedia({audio: config.audio, video: config.video}, function(s) { self.gotUserMediaForInvite(s); }, function(e) { self.getUserMediaFailed(e); }); + this.state = 'wait_local_media'; this.direction = 'outbound'; + this.config = config; }; MatrixCall.prototype.initWithInvite = function(msg) { this.msg = msg; - this.peerConn = new window.RTCPeerConnection({"iceServers":[{"urls":"stun:stun.l.google.com:19302"}]}) - self= this; - this.peerConn.oniceconnectionstatechange = function() { self.onIceConnectionStateChanged(); }; - this.peerConn.onicecandidate = function(c) { self.gotLocalIceCandidate(c); }; - this.peerConn.onsignalingstatechange = function() { self.onSignallingStateChanged(); }; - this.peerConn.onaddstream = function(s) { self.onAddStream(s); }; - this.peerConn.setRemoteDescription(new RTCSessionDescription(this.msg.offer), self.onSetRemoteDescriptionSuccess, self.onSetRemoteDescriptionError); + this.peerConn = this.createPeerConnection(); + this.peerConn.setRemoteDescription(new RTCSessionDescription(this.msg.offer), this.onSetRemoteDescriptionSuccess, this.onSetRemoteDescriptionError); this.state = 'ringing'; this.direction = 'inbound'; }; MatrixCall.prototype.answer = function() { - console.trace("Answering call "+this.call_id); - self = this; - navigator.getUserMedia({audio: true, video: false}, function(s) { self.gotUserMediaForAnswer(s); }, function(e) { self.getUserMediaFailed(e); }); - this.state = 'wait_local_media'; + console.log("Answering call "+this.call_id); + var self = this; + if (!this.localAVStream && !this.waitForLocalAVStream) { + navigator.getUserMedia({audio: true, video: false}, function(s) { self.gotUserMediaForAnswer(s); }, function(e) { self.getUserMediaFailed(e); }); + this.state = 'wait_local_media'; + } else if (this.localAVStream) { + this.gotUserMediaForAnswer(this.localAVStream); + } else if (this.waitForLocalAVStream) { + this.state = 'wait_local_media'; + } }; MatrixCall.prototype.stopAllMedia = function() { if (this.localAVStream) { forAllTracksOnStream(this.localAVStream, function(t) { - t.stop(); + if (t.stop) t.stop(); }); } if (this.remoteAVStream) { forAllTracksOnStream(this.remoteAVStream, function(t) { - t.stop(); + if (t.stop) t.stop(); }); } }; - MatrixCall.prototype.hangup = function() { - console.trace("Ending call "+this.call_id); + MatrixCall.prototype.hangup = function(suppressEvent) { + console.log("Ending call "+this.call_id); this.stopAllMedia(); + if (this.peerConn) this.peerConn.close(); + + this.hangupParty = 'local'; var content = { version: 0, call_id: this.call_id, }; - matrixService.sendEvent(this.room_id, 'm.call.hangup', undefined, content).then(this.messageSent, this.messageSendFailed); + this.sendEventWithRetry('m.call.hangup', content); this.state = 'ended'; + if (this.onHangup && !suppressEvent) this.onHangup(this); }; MatrixCall.prototype.gotUserMediaForInvite = function(stream) { + if (this.successor) { + this.successor.gotUserMediaForAnswer(stream); + return; + } + if (this.state == 'ended') return; + this.localAVStream = stream; var audioTracks = stream.getAudioTracks(); for (var i = 0; i < audioTracks.length; i++) { audioTracks[i].enabled = true; } - this.peerConn = new window.RTCPeerConnection({"iceServers":[{"urls":"stun:stun.l.google.com:19302"}]}) - self = this; - this.peerConn.oniceconnectionstatechange = function() { self.onIceConnectionStateChanged(); }; - this.peerConn.onsignalingstatechange = function() { self.onSignallingStateChanged(); }; - this.peerConn.onicecandidate = function(c) { self.gotLocalIceCandidate(c); }; - this.peerConn.onaddstream = function(s) { self.onAddStream(s); }; + this.peerConn = this.createPeerConnection(); this.peerConn.addStream(stream); + var self = this; this.peerConn.createOffer(function(d) { self.gotLocalOffer(d); }, function(e) { self.getLocalOfferFailed(e); }); - this.state = 'create_offer'; + $rootScope.$apply(function() { + self.state = 'create_offer'; + }); }; MatrixCall.prototype.gotUserMediaForAnswer = function(stream) { + if (this.state == 'ended') return; + this.localAVStream = stream; var audioTracks = stream.getAudioTracks(); for (var i = 0; i < audioTracks.length; i++) { audioTracks[i].enabled = true; } this.peerConn.addStream(stream); - self = this; + var self = this; var constraints = { 'mandatory': { 'OfferToReceiveAudio': true, @@ -138,23 +167,28 @@ angular.module('MatrixCall', []) }, }; this.peerConn.createAnswer(function(d) { self.createdAnswer(d); }, function(e) {}, constraints); - this.state = 'create_answer'; + // This can't be in an apply() because it's called by a predecessor call under glare conditions :( + self.state = 'create_answer'; }; MatrixCall.prototype.gotLocalIceCandidate = function(event) { - console.trace(event); + console.log(event); if (event.candidate) { var content = { version: 0, call_id: this.call_id, candidate: event.candidate }; - matrixService.sendEvent(this.room_id, 'm.call.candidate', undefined, content).then(this.messageSent, this.messageSendFailed); + this.sendEventWithRetry('m.call.candidate', content); } } MatrixCall.prototype.gotRemoteIceCandidate = function(cand) { - console.trace("Got ICE candidate from remote: "+cand); + console.log("Got ICE candidate from remote: "+cand); + if (this.state == 'ended') { + console.log("Ignoring remote ICE candidate because call has ended"); + return; + } var candidateObject = new RTCIceCandidate({ sdpMLineIndex: cand.label, candidate: cand.candidate @@ -163,12 +197,18 @@ angular.module('MatrixCall', []) }; MatrixCall.prototype.receivedAnswer = function(msg) { - this.peerConn.setRemoteDescription(new RTCSessionDescription(msg.answer), self.onSetRemoteDescriptionSuccess, self.onSetRemoteDescriptionError); + this.peerConn.setRemoteDescription(new RTCSessionDescription(msg.answer), this.onSetRemoteDescriptionSuccess, this.onSetRemoteDescriptionError); this.state = 'connecting'; }; MatrixCall.prototype.gotLocalOffer = function(description) { - console.trace("Created offer: "+description); + console.log("Created offer: "+description); + + if (this.state == 'ended') { + console.log("Ignoring newly created offer on call ID "+this.call_id+" because the call has ended"); + return; + } + this.peerConn.setLocalDescription(description); var content = { @@ -176,26 +216,27 @@ angular.module('MatrixCall', []) call_id: this.call_id, offer: description }; - matrixService.sendEvent(this.room_id, 'm.call.invite', undefined, content).then(this.messageSent, this.messageSendFailed); - this.state = 'invite_sent'; + this.sendEventWithRetry('m.call.invite', content); + + var self = this; + $rootScope.$apply(function() { + self.state = 'invite_sent'; + }); }; MatrixCall.prototype.createdAnswer = function(description) { - console.trace("Created answer: "+description); + console.log("Created answer: "+description); this.peerConn.setLocalDescription(description); var content = { version: 0, call_id: this.call_id, answer: description }; - matrixService.sendEvent(this.room_id, 'm.call.answer', undefined, content).then(this.messageSent, this.messageSendFailed); - this.state = 'connecting'; - }; - - MatrixCall.prototype.messageSent = function() { - }; - - MatrixCall.prototype.messageSendFailed = function(error) { + this.sendEventWithRetry('m.call.answer', content); + var self = this; + $rootScope.$apply(function() { + self.state = 'connecting'; + }); }; MatrixCall.prototype.getLocalOfferFailed = function(error) { @@ -204,33 +245,36 @@ angular.module('MatrixCall', []) MatrixCall.prototype.getUserMediaFailed = function() { this.onError("Couldn't start capturing audio! Is your microphone set up?"); + this.hangup(); }; MatrixCall.prototype.onIceConnectionStateChanged = function() { if (this.state == 'ended') return; // because ICE can still complete as we're ending the call - console.trace("Ice connection state changed to: "+this.peerConn.iceConnectionState); + console.log("Ice connection state changed to: "+this.peerConn.iceConnectionState); // ideally we'd consider the call to be connected when we get media but chrome doesn't implement nay of the 'onstarted' events yet if (this.peerConn.iceConnectionState == 'completed' || this.peerConn.iceConnectionState == 'connected') { - this.state = 'connected'; - this.didConnect = true; - $rootScope.$apply(); + var self = this; + $rootScope.$apply(function() { + self.state = 'connected'; + self.didConnect = true; + }); } }; MatrixCall.prototype.onSignallingStateChanged = function() { - console.trace("Signalling state changed to: "+this.peerConn.signalingState); + console.log("call "+this.call_id+": Signalling state changed to: "+this.peerConn.signalingState); }; MatrixCall.prototype.onSetRemoteDescriptionSuccess = function() { - console.trace("Set remote description"); + console.log("Set remote description"); }; MatrixCall.prototype.onSetRemoteDescriptionError = function(e) { - console.trace("Failed to set remote description"+e); + console.log("Failed to set remote description"+e); }; MatrixCall.prototype.onAddStream = function(event) { - console.trace("Stream added"+event); + console.log("Stream added"+event); var s = event.stream; @@ -251,23 +295,79 @@ angular.module('MatrixCall', []) }; MatrixCall.prototype.onRemoteStreamStarted = function(event) { - this.state = 'connected'; + var self = this; + $rootScope.$apply(function() { + self.state = 'connected'; + }); }; MatrixCall.prototype.onRemoteStreamEnded = function(event) { - this.state = 'ended'; - this.stopAllMedia(); - this.onHangup(); + console.log("Remote stream ended"); + var self = this; + $rootScope.$apply(function() { + self.state = 'ended'; + self.hangupParty = 'remote'; + self.stopAllMedia(); + if (self.peerConn.signalingState != 'closed') self.peerConn.close(); + if (self.onHangup) self.onHangup(self); + }); }; MatrixCall.prototype.onRemoteStreamTrackStarted = function(event) { - this.state = 'connected'; + var self = this; + $rootScope.$apply(function() { + self.state = 'connected'; + }); }; MatrixCall.prototype.onHangupReceived = function() { + console.log("Hangup received"); this.state = 'ended'; + this.hangupParty = 'remote'; this.stopAllMedia(); - this.onHangup(); + if (this.peerConn.signalingState != 'closed') this.peerConn.close(); + if (this.onHangup) this.onHangup(this); + }; + + MatrixCall.prototype.replacedBy = function(newCall) { + console.log(this.call_id+" being replaced by "+newCall.call_id); + if (this.state == 'wait_local_media') { + console.log("Telling new call to wait for local media"); + newCall.waitForLocalAVStream = true; + } else if (this.state == 'create_offer') { + console.log("Handing local stream to new call"); + newCall.localAVStream = this.localAVStream; + delete(this.localAVStream); + } else if (this.state == 'invite_sent') { + console.log("Handing local stream to new call"); + newCall.localAVStream = this.localAVStream; + delete(this.localAVStream); + } + this.successor = newCall; + this.hangup(true); + }; + + MatrixCall.prototype.sendEventWithRetry = function(evType, content) { + var ev = { type:evType, content:content, tries:1 }; + var self = this; + matrixService.sendEvent(this.room_id, evType, undefined, content).then(this.eventSent, function(error) { self.eventSendFailed(ev, error); } ); + }; + + MatrixCall.prototype.eventSent = function() { + }; + + MatrixCall.prototype.eventSendFailed = function(ev, error) { + if (ev.tries > 5) { + console.log("Failed to send event of type "+ev.type+" on attempt "+ev.tries+". Giving up."); + return; + } + var delayMs = 500 * Math.pow(2, ev.tries); + console.log("Failed to send event of type "+ev.type+". Retrying in "+delayMs+"ms"); + ++ev.tries; + var self = this; + $timeout(function() { + matrixService.sendEvent(self.room_id, ev.type, undefined, ev.content).then(self.eventSent, function(error) { self.eventSendFailed(ev, error); } ); + }, delayMs); }; return MatrixCall; diff --git a/webclient/components/matrix/matrix-filter.js b/webclient/components/matrix/matrix-filter.js index 260e0827df..015a88bcad 100644 --- a/webclient/components/matrix/matrix-filter.js +++ b/webclient/components/matrix/matrix-filter.js @@ -31,15 +31,17 @@ angular.module('matrixFilter', []) } if (undefined === roomName) { - // Else, build the name from its users + var room = $rootScope.events.rooms[room_id]; if (room) { + // Get name from room state date var room_name_event = room["m.room.name"]; - if (room_name_event) { roomName = room_name_event.content.name; } else if (room.members) { + // Else, build the name from its users + // FIXME: Is it still required? // Limit the room renaming to 1:1 room if (2 === Object.keys(room.members).length) { for (var i in room.members) { diff --git a/webclient/components/matrix/matrix-phone-service.js b/webclient/components/matrix/matrix-phone-service.js index ca86b473e7..b0dcf19100 100644 --- a/webclient/components/matrix/matrix-phone-service.js +++ b/webclient/components/matrix/matrix-phone-service.js @@ -22,6 +22,7 @@ angular.module('matrixPhoneService', []) }; matrixPhoneService.INCOMING_CALL_EVENT = "INCOMING_CALL_EVENT"; + matrixPhoneService.REPLACED_CALL_EVENT = "REPLACED_CALL_EVENT"; matrixPhoneService.allCalls = {}; matrixPhoneService.callPlaced = function(call) { @@ -38,29 +39,59 @@ angular.module('matrixPhoneService', []) call.call_id = msg.call_id; call.initWithInvite(msg); matrixPhoneService.allCalls[call.call_id] = call; - $rootScope.$broadcast(matrixPhoneService.INCOMING_CALL_EVENT, call); + + // Were we trying to call that user (room)? + var existingCall; + var callIds = Object.keys(matrixPhoneService.allCalls); + for (var i = 0; i < callIds.length; ++i) { + var thisCallId = callIds[i]; + var thisCall = matrixPhoneService.allCalls[thisCallId]; + + if (call.room_id == thisCall.room_id && thisCall.direction == 'outbound' + && (thisCall.state == 'wait_local_media' || thisCall.state == 'create_offer' || thisCall.state == 'invite_sent')) { + existingCall = thisCall; + break; + } + } + + if (existingCall) { + // If we've only got to wait_local_media or create_offer and we've got an invite, + // pick the incoming call because we know we haven't sent our invite yet + // otherwise, pick whichever call has the lowest call ID (by string comparison) + if (existingCall.state == 'wait_local_media' || existingCall.state == 'create_offer' || existingCall.call_id > call.call_id) { + console.log("Glare detected: answering incoming call "+call.call_id+" and canceling outgoing call "+existingCall.call_id); + existingCall.replacedBy(call); + call.answer(); + $rootScope.$broadcast(matrixPhoneService.REPLACED_CALL_EVENT, existingCall, call); + } else { + console.log("Glare detected: rejecting incoming call "+call.call_id+" and keeping outgoing call "+existingCall.call_id); + call.hangup(); + } + } else { + $rootScope.$broadcast(matrixPhoneService.INCOMING_CALL_EVENT, call); + } } else if (event.type == 'm.call.answer') { var call = matrixPhoneService.allCalls[msg.call_id]; if (!call) { - console.trace("Got answer for unknown call ID "+msg.call_id); + console.log("Got answer for unknown call ID "+msg.call_id); return; } call.receivedAnswer(msg); } else if (event.type == 'm.call.candidate') { var call = matrixPhoneService.allCalls[msg.call_id]; if (!call) { - console.trace("Got candidate for unknown call ID "+msg.call_id); + console.log("Got candidate for unknown call ID "+msg.call_id); return; } call.gotRemoteIceCandidate(msg.candidate); } else if (event.type == 'm.call.hangup') { var call = matrixPhoneService.allCalls[msg.call_id]; if (!call) { - console.trace("Got hangup for unknown call ID "+msg.call_id); + console.log("Got hangup for unknown call ID "+msg.call_id); return; } call.onHangupReceived(); - matrixPhoneService.allCalls[msg.call_id] = undefined; + delete(matrixPhoneService.allCalls[msg.call_id]); } }); diff --git a/webclient/components/matrix/matrix-service.js b/webclient/components/matrix/matrix-service.js index 3c28c52fbe..68ef16800b 100644 --- a/webclient/components/matrix/matrix-service.js +++ b/webclient/components/matrix/matrix-service.js @@ -130,8 +130,9 @@ angular.module('matrixService', []) return doRequest("POST", path, undefined, req); }, - // List all rooms joined or been invited to - rooms: function(limit, feedback) { + // Get the user's current state: his presence, the list of his rooms with + // the last {limit} events + initialSync: function(limit, feedback) { // The REST path spec var path = "/initialSync"; @@ -234,6 +235,32 @@ angular.module('matrixService', []) return doRequest("GET", path, undefined, {}); }, + + setName: function(room_id, name) { + var data = { + name: name + }; + return this.sendStateEvent(room_id, "m.room.name", data); + }, + + setTopic: function(room_id, topic) { + var data = { + topic: topic + }; + return this.sendStateEvent(room_id, "m.room.topic", data); + }, + + + sendStateEvent: function(room_id, eventType, content, state_key) { + var path = "/rooms/$room_id/state/"+eventType; + if (state_key !== undefined) { + path += "/" + state_key; + } + room_id = encodeURIComponent(room_id); + path = path.replace("$room_id", room_id); + + return doRequest("PUT", path, undefined, content); + }, sendEvent: function(room_id, eventType, txn_id, content) { // The REST path spec diff --git a/webclient/home/home-controller.js b/webclient/home/home-controller.js index 85e8990c29..c0c4ea11aa 100644 --- a/webclient/home/home-controller.js +++ b/webclient/home/home-controller.js @@ -142,4 +142,9 @@ angular.module('HomeController', ['matrixService', 'eventHandlerService', 'Recen refresh(); }; + + // Clean data when user logs out + $scope.$on(eventHandlerService.RESET_EVENT, function() { + $scope.public_rooms = []; + }); }]); diff --git a/webclient/home/home.html b/webclient/home/home.html index 7240e79f86..5a1e18e1de 100644 --- a/webclient/home/home.html +++ b/webclient/home/home.html @@ -24,11 +24,30 @@ <h3>Public rooms</h3> - <div class="public_rooms" ng-repeat="room in public_rooms"> - <div> - <a href="#/room/{{ room.room_alias ? room.room_alias : room.room_id }}" >{{ room.room_display_name }}</a> - </div> - </div> + <table class="publicTable"> + <tbody ng-repeat="room in public_rooms | orderBy:'room_display_name'" + class="publicRoomEntry" + ng-class="room.room_display_name.toLowerCase().indexOf('#matrix:') === 0 ? 'roomHighlight' : ''"> + <tr> + <td class="publicRoomEntry"> + <a href="#/room/{{ room.room_alias ? room.room_alias : room.room_id }}" > + {{ room.room_display_name }} + </a> + </td> + <td> + <div class="publicRoomJoinedUsers" + ng-show="room.num_joined_members"> + {{ room.num_joined_members }} {{ room.num_joined_members == 1 ? 'user' : 'users' }} + </div> + </td> + </tr> + <tr> + <td colspan="2" class="publicRoomTopic"> + {{ room.topic }} + </td> + </tr> + </tbody> + </table> <br/> <div> diff --git a/webclient/index.html b/webclient/index.html index 81c7c7d06c..9eea08215c 100644 --- a/webclient/index.html +++ b/webclient/index.html @@ -15,7 +15,9 @@ <script src="js/angular.min.js"></script> <script src="js/angular-route.min.js"></script> <script src="js/angular-sanitize.min.js"></script> + <script src="js/angular-animate.min.js"></script> <script type='text/javascript' src='js/ng-infinite-scroll-matrix.js'></script> + <script type='text/javascript' src='js/autofill-event.js'></script> <script src="app.js"></script> <script src="config.js"></script> <script src="app-controller.js"></script> @@ -56,9 +58,11 @@ <br /> <span id="callState"> <span ng-show="currentCall.state == 'invite_sent'">Calling...</span> + <span ng-show="currentCall.state == 'ringing'">Incoming Call</span> <span ng-show="currentCall.state == 'connecting'">Call Connecting...</span> <span ng-show="currentCall.state == 'connected'">Call Connected</span> - <span ng-show="currentCall.state == 'ended' && !currentCall.didConnect && currentCall.direction == 'outbound'">Call Rejected</span> + <span ng-show="currentCall.state == 'ended' && !currentCall.didConnect && currentCall.direction == 'outbound' && currentCall.hangupParty == 'remote'">Call Rejected</span> + <span ng-show="currentCall.state == 'ended' && !currentCall.didConnect && currentCall.direction == 'outbound' && currentCall.hangupParty == 'local'">Call Canceled</span> <span ng-show="currentCall.state == 'ended' && currentCall.didConnect && currentCall.direction == 'outbound'">Call Ended</span> <span ng-show="currentCall.state == 'ended' && !currentCall.didConnect && currentCall.direction == 'inbound'">Call Canceled</span> <span ng-show="currentCall.state == 'ended' && currentCall.didConnect && currentCall.direction == 'inbound'">Call Ended</span> @@ -70,6 +74,22 @@ <button ng-click="hangupCall()">Reject</button> </span> <button ng-click="hangupCall()" ng-show="currentCall && currentCall.state != 'ringing' && currentCall.state != 'ended' && currentCall.state != 'fledgling'">Hang up</button> + <audio id="ringAudio" loop> + <source src="media/ring.ogg" type="audio/ogg" /> + <source src="media/ring.mp3" type="audio/mpeg" /> + </audio> + <audio id="ringbackAudio" loop> + <source src="media/ringback.ogg" type="audio/ogg" /> + <source src="media/ringback.mp3" type="audio/mpeg" /> + </audio> + <audio id="callendAudio"> + <source src="media/callend.ogg" type="audio/ogg" /> + <source src="media/callend.mp3" type="audio/mpeg" /> + </audio> + <audio id="busyAudio"> + <source src="media/busy.ogg" type="audio/ogg" /> + <source src="media/busy.mp3" type="audio/mpeg" /> + </audio> </div> <a href id="headerUserId" ng-click='goToUserPage(user_id)'>{{ user_id }}</a> diff --git a/webclient/js/angular-animate.js b/webclient/js/angular-animate.js index bea4bc5232..c15f793c1b 100644 --- a/webclient/js/angular-animate.js +++ b/webclient/js/angular-animate.js @@ -1,5 +1,5 @@ /** - * @license AngularJS v1.2.20 + * @license AngularJS v1.3.0-rc.1 * (c) 2010-2014 Google, Inc. http://angularjs.org * License: MIT */ @@ -12,11 +12,8 @@ * @name ngAnimate * @description * - * # ngAnimate - * * The `ngAnimate` module provides support for JavaScript, CSS3 transition and CSS3 keyframe animation hooks within existing core and custom directives. * - * * <div doc-module-components="ngAnimate"></div> * * # Usage @@ -28,17 +25,18 @@ * * Below is a more detailed breakdown of the supported animation events provided by pre-existing ng directives: * - * | Directive | Supported Animations | - * |---------------------------------------------------------- |----------------------------------------------------| - * | {@link ng.directive:ngRepeat#usage_animations ngRepeat} | enter, leave and move | - * | {@link ngRoute.directive:ngView#usage_animations ngView} | enter and leave | - * | {@link ng.directive:ngInclude#usage_animations ngInclude} | enter and leave | - * | {@link ng.directive:ngSwitch#usage_animations ngSwitch} | enter and leave | - * | {@link ng.directive:ngIf#usage_animations ngIf} | enter and leave | - * | {@link ng.directive:ngClass#usage_animations ngClass} | add and remove | - * | {@link ng.directive:ngShow#usage_animations ngShow & ngHide} | add and remove (the ng-hide class value) | - * | {@link ng.directive:form#usage_animations form} | add and remove (dirty, pristine, valid, invalid & all other validations) | - * | {@link ng.directive:ngModel#usage_animations ngModel} | add and remove (dirty, pristine, valid, invalid & all other validations) | + * | Directive | Supported Animations | + * |-----------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------| + * | {@link ng.directive:ngRepeat#usage_animations ngRepeat} | enter, leave and move | + * | {@link ngRoute.directive:ngView#usage_animations ngView} | enter and leave | + * | {@link ng.directive:ngInclude#usage_animations ngInclude} | enter and leave | + * | {@link ng.directive:ngSwitch#usage_animations ngSwitch} | enter and leave | + * | {@link ng.directive:ngIf#usage_animations ngIf} | enter and leave | + * | {@link ng.directive:ngClass#usage_animations ngClass} | add and remove (the CSS class(es) present) | + * | {@link ng.directive:ngShow#usage_animations ngShow} & {@link ng.directive:ngHide#usage_animations ngHide} | add and remove (the ng-hide class value) | + * | {@link ng.directive:form#usage_animations form} & {@link ng.directive:ngModel#usage_animations ngModel} | add and remove (dirty, pristine, valid, invalid & all other validations) | + * | {@link ngMessages.directive:ngMessage#usage_animations ngMessages} | add and remove (ng-active & ng-inactive) | + * | {@link ngMessages.directive:ngMessage#usage_animations ngMessage} | enter and leave | * * You can find out more information about animations upon visiting each directive page. * @@ -52,9 +50,9 @@ * } * * .slide.ng-enter { } /* starting animations for enter */ - * .slide.ng-enter-active { } /* terminal animations for enter */ + * .slide.ng-enter.ng-enter-active { } /* terminal animations for enter */ * .slide.ng-leave { } /* starting animations for leave */ - * .slide.ng-leave-active { } /* terminal animations for leave */ + * .slide.ng-leave.ng-leave-active { } /* terminal animations for leave */ * </style> * * <!-- @@ -81,6 +79,16 @@ * When the `on` expression value changes and an animation is triggered then each of the elements within * will all animate without the block being applied to child elements. * + * ## Are animations run when the application starts? + * No they are not. When an application is bootstrapped Angular will disable animations from running to avoid + * a frenzy of animations from being triggered as soon as the browser has rendered the screen. For this to work, + * Angular will wait for two digest cycles until enabling animations. From there on, any animation-triggering + * layout changes in the application will trigger animations as normal. + * + * In addition, upon bootstrap, if the routing system or any directives or load remote data (via $http) then Angular + * will automatically extend the wait time to enable animations once **all** of the outbound HTTP requests + * are complete. + * * <h2>CSS-defined Animations</h2> * The animate service will automatically apply two CSS classes to the animated element and these two CSS classes * are designed to contain the start and end CSS styling. Both CSS transitions and keyframe animations are supported @@ -150,7 +158,74 @@ * immediately resulting in a DOM element that is at its final state. This final state is when the DOM element * has no CSS transition/animation classes applied to it. * - * <h3>CSS Staggering Animations</h3> + * ### Structural transition animations + * + * Structural transitions (such as enter, leave and move) will always apply a `0s none` transition + * value to force the browser into rendering the styles defined in the setup (.ng-enter, .ng-leave + * or .ng-move) class. This means that any active transition animations operating on the element + * will be cut off to make way for the enter, leave or move animation. + * + * ### Class-based transition animations + * + * Class-based transitions refer to transition animations that are triggered when a CSS class is + * added to or removed from the element (via `$animate.addClass`, `$animate.removeClass`, + * `$animate.setClass`, or by directives such as `ngClass`, `ngModel` and `form`). + * They are different when compared to structural animations since they **do not cancel existing + * animations** nor do they **block successive transitions** from rendering on the same element. + * This distinction allows for **multiple class-based transitions** to be performed on the same element. + * + * In addition to ngAnimate supporting the default (natural) functionality of class-based transition + * animations, ngAnimate also decorates the element with starting and ending CSS classes to aid the + * developer in further styling the element throughout the transition animation. Earlier versions + * of ngAnimate may have caused natural CSS transitions to break and not render properly due to + * $animate temporarily blocking transitions using `0s none` in order to allow the setup CSS class + * (the `-add` or `-remove` class) to be applied without triggering an animation. However, as of + * **version 1.3**, this workaround has been removed with ngAnimate and all non-ngAnimate CSS + * class transitions are compatible with ngAnimate. + * + * There is, however, one special case when dealing with class-based transitions in ngAnimate. + * When rendering class-based transitions that make use of the setup and active CSS classes + * (e.g. `.fade-add` and `.fade-add-active` for when `.fade` is added) be sure to define + * the transition value **on the active CSS class** and not the setup class. + * + * ```css + * .fade-add { + * /* remember to place a 0s transition here + * to ensure that the styles are applied instantly + * even if the element already has a transition style */ + * transition:0s linear all; + * + * /* starting CSS styles */ + * opacity:1; + * } + * .fade-add.fade-add-active { + * /* this will be the length of the animation */ + * transition:1s linear all; + * opacity:0; + * } + * ``` + * + * The setup CSS class (in this case `.fade-add`) also has a transition style property, however, it + * has a duration of zero. This may not be required, however, incase the browser is unable to render + * the styling present in this CSS class instantly then it could be that the browser is attempting + * to perform an unnecessary transition. + * + * This workaround, however, does not apply to standard class-based transitions that are rendered + * when a CSS class containing a transition is applied to an element: + * + * ```css + * .fade { + * /* this works as expected */ + * transition:1s linear all; + * opacity:0; + * } + * ``` + * + * Please keep this in mind when coding the CSS markup that will be used within class-based transitions. + * Also, try not to mix the two class-based animation flavors together since the CSS code may become + * overly complex. + * + * ### CSS Staggering Animations * A Staggering animation is a collection of animations that are issued with a slight delay in between each successive operation resulting in a * curtain-like effect. The ngAnimate module, as of 1.2.0, supports staggering animations and the stagger effect can be * performed by creating a **ng-EVENT-stagger** CSS class and attaching that class to the base CSS class used for @@ -205,7 +280,7 @@ * * Stagger animations are currently only supported within CSS-defined animations. * - * <h2>JavaScript-defined Animations</h2> + * ## JavaScript-defined Animations * In the event that you do not want to use CSS3 transitions or CSS3 animations or if you wish to offer animations on browsers that do not * yet support CSS transitions/animations, then you can make use of JavaScript animations defined inside of your AngularJS module. * @@ -273,7 +348,7 @@ angular.module('ngAnimate', ['ng']) var NG_ANIMATE_CHILDREN = '$$ngAnimateChildren'; return function(scope, element, attrs) { var val = attrs.ngAnimateChildren; - if(angular.isString(val) && val.length === 0) { //empty attribute + if (angular.isString(val) && val.length === 0) { //empty attribute element.data(NG_ANIMATE_CHILDREN, true); } else { scope.$watch(val, function(value) { @@ -307,6 +382,7 @@ angular.module('ngAnimate', ['ng']) var noop = angular.noop; var forEach = angular.forEach; var selectors = $animateProvider.$$selectors; + var isArray = angular.isArray; var ELEMENT_NODE = 1; var NG_ANIMATE_STATE = '$$ngAnimateState'; @@ -317,7 +393,7 @@ angular.module('ngAnimate', ['ng']) function extractElementNode(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; } } @@ -335,24 +411,38 @@ angular.module('ngAnimate', ['ng']) return extractElementNode(elm1) == extractElementNode(elm2); } - $provide.decorator('$animate', ['$delegate', '$injector', '$sniffer', '$rootElement', '$$asyncCallback', '$rootScope', '$document', - function($delegate, $injector, $sniffer, $rootElement, $$asyncCallback, $rootScope, $document) { + $provide.decorator('$animate', + ['$delegate', '$$q', '$injector', '$sniffer', '$rootElement', '$$asyncCallback', '$rootScope', '$document', '$templateRequest', + function($delegate, $$q, $injector, $sniffer, $rootElement, $$asyncCallback, $rootScope, $document, $templateRequest) { - var globalAnimationCounter = 0; $rootElement.data(NG_ANIMATE_STATE, rootAnimateState); - // disable animations during bootstrap, but once we bootstrapped, wait again - // for another digest until enabling animations. The reason why we digest twice - // is because all structural animations (enter, leave and move) all perform a - // post digest operation before animating. If we only wait for a single digest - // to pass then the structural animation would render its animation on page load. - // (which is what we're trying to avoid when the application first boots up.) - $rootScope.$$postDigest(function() { - $rootScope.$$postDigest(function() { - rootAnimateState.running = false; - }); - }); + // Wait until all directive and route-related templates are downloaded and + // compiled. The $templateRequest.totalPendingRequests variable keeps track of + // all of the remote templates being currently downloaded. If there are no + // templates currently downloading then the watcher will still fire anyway. + var deregisterWatch = $rootScope.$watch( + function() { return $templateRequest.totalPendingRequests; }, + function(val, oldVal) { + if (val !== 0) return; + deregisterWatch(); + + // Now that all templates have been downloaded, $animate will wait until + // the post digest queue is empty before enabling animations. By having two + // calls to $postDigest calls we can ensure that the flag is enabled at the + // very end of the post digest queue. Since all of the animations in $animate + // use $postDigest, it's important that the code below executes at the end. + // This basically means that the page is fully downloaded and compiled before + // any animations are triggered. + $rootScope.$$postDigest(function() { + $rootScope.$$postDigest(function() { + rootAnimateState.running = false; + }); + }); + } + ); + var globalAnimationCounter = 0; var classNameFilter = $animateProvider.classNameFilter(); var isAnimatableClassName = !classNameFilter ? function() { return true; } @@ -360,10 +450,81 @@ angular.module('ngAnimate', ['ng']) return classNameFilter.test(className); }; - function blockElementAnimations(element) { + function classBasedAnimationsBlocked(element, setter) { var data = element.data(NG_ANIMATE_STATE) || {}; - data.running = true; - element.data(NG_ANIMATE_STATE, data); + if (setter) { + data.running = true; + data.structural = true; + element.data(NG_ANIMATE_STATE, data); + } + return data.disabled || (data.running && data.structural); + } + + function runAnimationPostDigest(fn) { + var cancelFn, defer = $$q.defer(); + defer.promise.$$cancelFn = function() { + cancelFn && cancelFn(); + }; + $rootScope.$$postDigest(function() { + cancelFn = fn(function() { + defer.resolve(); + }); + }); + return defer.promise; + } + + function resolveElementClasses(element, cache, runningAnimations) { + runningAnimations = runningAnimations || {}; + var map = {}; + + forEach(cache.add, function(className) { + if (className && className.length) { + map[className] = map[className] || 0; + map[className]++; + } + }); + + forEach(cache.remove, function(className) { + if (className && className.length) { + map[className] = map[className] || 0; + map[className]--; + } + }); + + var lookup = []; + forEach(runningAnimations, function(data, selector) { + forEach(selector.split(' '), function(s) { + lookup[s]=data; + }); + }); + + var toAdd = [], toRemove = []; + forEach(map, function(status, className) { + var hasClass = angular.$$hasClass(element[0], className); + var matchingAnimation = lookup[className] || {}; + + // When addClass and removeClass is called then $animate will check to + // see if addClass and removeClass cancel each other out. When there are + // more calls to removeClass than addClass then the count falls below 0 + // and then the removeClass animation will be allowed. Otherwise if the + // count is above 0 then that means an addClass animation will commence. + // Once an animation is allowed then the code will also check to see if + // there exists any on-going animation that is already adding or remvoing + // the matching CSS class. + if (status < 0) { + //does it have the class or will it have the class + if (hasClass || matchingAnimation.event == 'addClass') { + toRemove.push(className); + } + } else if (status > 0) { + //is the class missing or will it be removed? + if (!hasClass || matchingAnimation.event == 'removeClass') { + toAdd.push(className); + } + } + }); + + return (toAdd.length + toRemove.length) > 0 && [toAdd.join(' '), toRemove.join(' ')]; } function lookup(name) { @@ -387,7 +548,7 @@ angular.module('ngAnimate', ['ng']) for(var i=0; i < classes.length; i++) { var klass = classes[i], selectorFactoryName = selectors[klass]; - if(selectorFactoryName && !flagMap[klass]) { + if (selectorFactoryName && !flagMap[klass]) { matches.push($injector.get(selectorFactoryName)); flagMap[klass] = true; } @@ -400,25 +561,34 @@ angular.module('ngAnimate', ['ng']) //transcluded directives may sometimes fire an animation using only comment nodes //best to catch this early on to prevent any animation operations from occurring var node = element[0]; - if(!node) { + if (!node) { return; } + var classNameAdd; + var classNameRemove; + if (isArray(className)) { + classNameAdd = className[0]; + classNameRemove = className[1]; + if (!classNameAdd) { + className = classNameRemove; + animationEvent = 'removeClass'; + } else if (!classNameRemove) { + className = classNameAdd; + animationEvent = 'addClass'; + } else { + className = classNameAdd + ' ' + classNameRemove; + } + } + var isSetClassOperation = animationEvent == 'setClass'; var isClassBased = isSetClassOperation || animationEvent == 'addClass' || animationEvent == 'removeClass'; - var classNameAdd, classNameRemove; - if(angular.isArray(className)) { - classNameAdd = className[0]; - classNameRemove = className[1]; - className = classNameAdd + ' ' + classNameRemove; - } - var currentClassName = element.attr('class'); var classes = currentClassName + ' ' + className; - if(!isAnimatableClassName(classes)) { + if (!isAnimatableClassName(classes)) { return; } @@ -432,7 +602,7 @@ angular.module('ngAnimate', ['ng']) var animationLookup = (' ' + classes).replace(/\s+/g,'.'); forEach(lookup(animationLookup), function(animationFactory) { var created = registerAnimation(animationFactory, animationEvent); - if(!created && isSetClassOperation) { + if (!created && isSetClassOperation) { registerAnimation(animationFactory, 'addClass'); registerAnimation(animationFactory, 'removeClass'); } @@ -441,8 +611,8 @@ angular.module('ngAnimate', ['ng']) function registerAnimation(animationFactory, event) { var afterFn = animationFactory[event]; var beforeFn = animationFactory['before' + event.charAt(0).toUpperCase() + event.substr(1)]; - if(afterFn || beforeFn) { - if(event == 'leave') { + if (afterFn || beforeFn) { + if (event == 'leave') { beforeFn = afterFn; //when set as null then animation knows to skip this phase afterFn = null; @@ -465,9 +635,9 @@ angular.module('ngAnimate', ['ng']) var count = 0; function afterAnimationComplete(index) { - if(cancellations) { + if (cancellations) { (cancellations[index] || noop)(); - if(++count < animations.length) return; + if (++count < animations.length) return; cancellations = null; } allCompleteFn(); @@ -496,7 +666,7 @@ angular.module('ngAnimate', ['ng']) } }); - if(cancellations && cancellations.length === 0) { + if (cancellations && cancellations.length === 0) { allCompleteFn(); } } @@ -522,13 +692,13 @@ angular.module('ngAnimate', ['ng']) }); }, cancel : function() { - if(beforeCancel) { + if (beforeCancel) { forEach(beforeCancel, function(cancelFn) { (cancelFn || noop)(true); }); beforeComplete(true); } - if(afterCancel) { + if (afterCancel) { forEach(afterCancel, function(cancelFn) { (cancelFn || noop)(true); }); @@ -541,7 +711,7 @@ angular.module('ngAnimate', ['ng']) /** * @ngdoc service * @name $animate - * @kind function + * @kind object * * @description * The `$animate` service provides animation detection support while performing DOM operations (enter, leave and move) as well as during addClass and removeClass operations. @@ -555,6 +725,46 @@ angular.module('ngAnimate', ['ng']) * Requires the {@link ngAnimate `ngAnimate`} module to be installed. * * Please visit the {@link ngAnimate `ngAnimate`} module overview page learn more about how to use animations in your application. + * ## Callback Promises + * With AngularJS 1.3, each of the animation methods, on the `$animate` service, return a promise when called. The + * promise itself is then resolved once the animation has completed itself, has been cancelled or has been + * skipped due to animations being disabled. (Note that even if the animation is cancelled it will still + * call the resolve function of the animation.) + * + * ```js + * $animate.enter(element, container).then(function() { + * //...this is called once the animation is complete... + * }); + * ``` + * + * Also note that, due to the nature of the callback promise, if any Angular-specific code (like changing the scope, + * location of the page, etc...) is executed within the callback promise then be sure to wrap the code using + * `$scope.$apply(...)`; + * + * ```js + * $animate.leave(element).then(function() { + * $scope.$apply(function() { + * $location.path('/new-page'); + * }); + * }); + * ``` + * + * An animation can also be cancelled by calling the `$animate.cancel(promise)` method with the provided + * promise that was returned when the animation was started. + * + * ```js + * var promise = $animate.addClass(element, 'super-long-animation').then(function() { + * //this will still be called even if cancelled + * }); + * + * element.on('click', function() { + * //tooo lazy to wait for the animation to end + * $animate.cancel(promise); + * }); + * ``` + * + * (Keep in mind that the promise cancellation is unique to `$animate` since promises in + * general cannot be cancelled.) * */ return { @@ -569,34 +779,36 @@ angular.module('ngAnimate', ['ng']) * * Below is a breakdown of each step that occurs during enter animation: * - * | Animation Step | What the element class attribute looks like | - * |----------------------------------------------------------------------------------------------|---------------------------------------------| - * | 1. $animate.enter(...) is called | class="my-animation" | - * | 2. element is inserted into the parentElement element or beside the afterElement element | class="my-animation" | - * | 3. $animate runs any JavaScript-defined animations on the element | class="my-animation ng-animate" | - * | 4. the .ng-enter class is added to the element | class="my-animation ng-animate ng-enter" | - * | 5. $animate scans the element styles to get the CSS transition/animation duration and delay | class="my-animation ng-animate ng-enter" | - * | 6. $animate waits for 10ms (this performs a reflow) | class="my-animation ng-animate ng-enter" | - * | 7. the .ng-enter-active and .ng-animate-active classes are added (this triggers the CSS transition/animation) | class="my-animation ng-animate ng-animate-active ng-enter ng-enter-active" | - * | 8. $animate waits for X milliseconds for the animation to complete | class="my-animation ng-animate ng-animate-active ng-enter ng-enter-active" | - * | 9. The animation ends and all generated CSS classes are removed from the element | class="my-animation" | - * | 10. The doneCallback() callback is fired (if provided) | class="my-animation" | + * | Animation Step | What the element class attribute looks like | + * |-------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------| + * | 1. $animate.enter(...) is called | class="my-animation" | + * | 2. element is inserted into the parentElement element or beside the afterElement element | class="my-animation" | + * | 3. $animate waits for the next digest to start the animation | class="my-animation ng-animate" | + * | 4. $animate runs the JavaScript-defined animations detected on the element | class="my-animation ng-animate" | + * | 5. the .ng-enter class is added to the element | class="my-animation ng-animate ng-enter" | + * | 6. $animate scans the element styles to get the CSS transition/animation duration and delay | class="my-animation ng-animate ng-enter" | + * | 7. $animate blocks all CSS transitions on the element to ensure the .ng-enter class styling is applied right away | class="my-animation ng-animate ng-enter" | + * | 8. $animate waits for a single animation frame (this performs a reflow) | class="my-animation ng-animate ng-enter" | + * | 9. $animate removes the CSS transition block placed on the element | class="my-animation ng-animate ng-enter" | + * | 10. the .ng-enter-active class is added (this triggers the CSS transition/animation) | class="my-animation ng-animate ng-enter ng-enter-active" | + * | 11. $animate waits for the animation to complete (via events and timeout) | class="my-animation ng-animate ng-enter ng-enter-active" | + * | 12. The animation ends and all generated CSS classes are removed from the element | class="my-animation" | + * | 13. The returned promise is resolved. | class="my-animation" | * * @param {DOMElement} element the element that will be the focus of the enter animation * @param {DOMElement} parentElement the parent element of the element that will be the focus of the enter animation * @param {DOMElement} afterElement the sibling element (which is the previous element) of the element that will be the focus of the enter animation - * @param {function()=} doneCallback the callback function that will be called once the animation is complete + * @return {Promise} the animation callback promise */ - enter : function(element, parentElement, afterElement, doneCallback) { + enter : function(element, parentElement, afterElement) { element = angular.element(element); parentElement = prepareElement(parentElement); afterElement = prepareElement(afterElement); - blockElementAnimations(element); + classBasedAnimationsBlocked(element, true); $delegate.enter(element, parentElement, afterElement); - $rootScope.$$postDigest(function() { - element = stripCommentsFromElement(element); - performAnimation('enter', 'ng-enter', element, parentElement, afterElement, noop, doneCallback); + return runAnimationPostDigest(function(done) { + return performAnimation('enter', 'ng-enter', stripCommentsFromElement(element), parentElement, afterElement, noop, done); }); }, @@ -611,30 +823,35 @@ angular.module('ngAnimate', ['ng']) * * Below is a breakdown of each step that occurs during leave animation: * - * | Animation Step | What the element class attribute looks like | - * |----------------------------------------------------------------------------------------------|---------------------------------------------| - * | 1. $animate.leave(...) is called | class="my-animation" | - * | 2. $animate runs any JavaScript-defined animations on the element | class="my-animation ng-animate" | - * | 3. the .ng-leave class is added to the element | class="my-animation ng-animate ng-leave" | - * | 4. $animate scans the element styles to get the CSS transition/animation duration and delay | class="my-animation ng-animate ng-leave" | - * | 5. $animate waits for 10ms (this performs a reflow) | class="my-animation ng-animate ng-leave" | - * | 6. the .ng-leave-active and .ng-animate-active classes is added (this triggers the CSS transition/animation) | class="my-animation ng-animate ng-animate-active ng-leave ng-leave-active" | - * | 7. $animate waits for X milliseconds for the animation to complete | class="my-animation ng-animate ng-animate-active ng-leave ng-leave-active" | - * | 8. The animation ends and all generated CSS classes are removed from the element | class="my-animation" | - * | 9. The element is removed from the DOM | ... | - * | 10. The doneCallback() callback is fired (if provided) | ... | + * | Animation Step | What the element class attribute looks like | + * |-------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------| + * | 1. $animate.leave(...) is called | class="my-animation" | + * | 2. $animate runs the JavaScript-defined animations detected on the element | class="my-animation ng-animate" | + * | 3. $animate waits for the next digest to start the animation | class="my-animation ng-animate" | + * | 4. the .ng-leave class is added to the element | class="my-animation ng-animate ng-leave" | + * | 5. $animate scans the element styles to get the CSS transition/animation duration and delay | class="my-animation ng-animate ng-leave" | + * | 6. $animate blocks all CSS transitions on the element to ensure the .ng-leave class styling is applied right away | class="my-animation ng-animate ng-leave” | + * | 7. $animate waits for a single animation frame (this performs a reflow) | class="my-animation ng-animate ng-leave" | + * | 8. $animate removes the CSS transition block placed on the element | class="my-animation ng-animate ng-leave” | + * | 9. the .ng-leave-active class is added (this triggers the CSS transition/animation) | class="my-animation ng-animate ng-leave ng-leave-active" | + * | 10. $animate waits for the animation to complete (via events and timeout) | class="my-animation ng-animate ng-leave ng-leave-active" | + * | 11. The animation ends and all generated CSS classes are removed from the element | class="my-animation" | + * | 12. The element is removed from the DOM | ... | + * | 13. The returned promise is resolved. | ... | * * @param {DOMElement} element the element that will be the focus of the leave animation - * @param {function()=} doneCallback the callback function that will be called once the animation is complete + * @return {Promise} the animation callback promise */ - leave : function(element, doneCallback) { + leave : function(element) { element = angular.element(element); + cancelChildAnimations(element); - blockElementAnimations(element); - $rootScope.$$postDigest(function() { - performAnimation('leave', 'ng-leave', stripCommentsFromElement(element), null, null, function() { + classBasedAnimationsBlocked(element, true); + this.enabled(false, element); + return runAnimationPostDigest(function(done) { + return performAnimation('leave', 'ng-leave', stripCommentsFromElement(element), null, null, function() { $delegate.leave(element); - }, doneCallback); + }, done); }); }, @@ -650,35 +867,37 @@ angular.module('ngAnimate', ['ng']) * * Below is a breakdown of each step that occurs during move animation: * - * | Animation Step | What the element class attribute looks like | - * |----------------------------------------------------------------------------------------------|---------------------------------------------| - * | 1. $animate.move(...) is called | class="my-animation" | - * | 2. element is moved into the parentElement element or beside the afterElement element | class="my-animation" | - * | 3. $animate runs any JavaScript-defined animations on the element | class="my-animation ng-animate" | - * | 4. the .ng-move class is added to the element | class="my-animation ng-animate ng-move" | - * | 5. $animate scans the element styles to get the CSS transition/animation duration and delay | class="my-animation ng-animate ng-move" | - * | 6. $animate waits for 10ms (this performs a reflow) | class="my-animation ng-animate ng-move" | - * | 7. the .ng-move-active and .ng-animate-active classes is added (this triggers the CSS transition/animation) | class="my-animation ng-animate ng-animate-active ng-move ng-move-active" | - * | 8. $animate waits for X milliseconds for the animation to complete | class="my-animation ng-animate ng-animate-active ng-move ng-move-active" | - * | 9. The animation ends and all generated CSS classes are removed from the element | class="my-animation" | - * | 10. The doneCallback() callback is fired (if provided) | class="my-animation" | + * | Animation Step | What the element class attribute looks like | + * |------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------| + * | 1. $animate.move(...) is called | class="my-animation" | + * | 2. element is moved into the parentElement element or beside the afterElement element | class="my-animation" | + * | 3. $animate waits for the next digest to start the animation | class="my-animation ng-animate" | + * | 4. $animate runs the JavaScript-defined animations detected on the element | class="my-animation ng-animate" | + * | 5. the .ng-move class is added to the element | class="my-animation ng-animate ng-move" | + * | 6. $animate scans the element styles to get the CSS transition/animation duration and delay | class="my-animation ng-animate ng-move" | + * | 7. $animate blocks all CSS transitions on the element to ensure the .ng-move class styling is applied right away | class="my-animation ng-animate ng-move” | + * | 8. $animate waits for a single animation frame (this performs a reflow) | class="my-animation ng-animate ng-move" | + * | 9. $animate removes the CSS transition block placed on the element | class="my-animation ng-animate ng-move” | + * | 10. the .ng-move-active class is added (this triggers the CSS transition/animation) | class="my-animation ng-animate ng-move ng-move-active" | + * | 11. $animate waits for the animation to complete (via events and timeout) | class="my-animation ng-animate ng-move ng-move-active" | + * | 12. The animation ends and all generated CSS classes are removed from the element | class="my-animation" | + * | 13. The returned promise is resolved. | class="my-animation" | * * @param {DOMElement} element the element that will be the focus of the move animation * @param {DOMElement} parentElement the parentElement element of the element that will be the focus of the move animation * @param {DOMElement} afterElement the sibling element (which is the previous element) of the element that will be the focus of the move animation - * @param {function()=} doneCallback the callback function that will be called once the animation is complete + * @return {Promise} the animation callback promise */ - move : function(element, parentElement, afterElement, doneCallback) { + move : function(element, parentElement, afterElement) { element = angular.element(element); parentElement = prepareElement(parentElement); afterElement = prepareElement(afterElement); cancelChildAnimations(element); - blockElementAnimations(element); + classBasedAnimationsBlocked(element, true); $delegate.move(element, parentElement, afterElement); - $rootScope.$$postDigest(function() { - element = stripCommentsFromElement(element); - performAnimation('move', 'ng-move', element, parentElement, afterElement, noop, doneCallback); + return runAnimationPostDigest(function(done) { + return performAnimation('move', 'ng-move', stripCommentsFromElement(element), parentElement, afterElement, noop, done); }); }, @@ -690,33 +909,29 @@ angular.module('ngAnimate', ['ng']) * Triggers a custom animation event based off the className variable and then attaches the className value to the element as a CSS class. * Unlike the other animation methods, the animate service will suffix the className value with {@type -add} in order to provide * the animate service the setup and active CSS classes in order to trigger the animation (this will be skipped if no CSS transitions - * or keyframes are defined on the -add or base CSS class). + * or keyframes are defined on the -add-active or base CSS class). * * Below is a breakdown of each step that occurs during addClass animation: * - * | Animation Step | What the element class attribute looks like | - * |------------------------------------------------------------------------------------------------|---------------------------------------------| - * | 1. $animate.addClass(element, 'super') is called | class="my-animation" | - * | 2. $animate runs any JavaScript-defined animations on the element | class="my-animation ng-animate" | - * | 3. the .super-add class are added to the element | class="my-animation ng-animate super-add" | - * | 4. $animate scans the element styles to get the CSS transition/animation duration and delay | class="my-animation ng-animate super-add" | - * | 5. $animate waits for 10ms (this performs a reflow) | class="my-animation ng-animate super-add" | - * | 6. the .super, .super-add-active and .ng-animate-active classes are added (this triggers the CSS transition/animation) | class="my-animation ng-animate ng-animate-active super super-add super-add-active" | - * | 7. $animate waits for X milliseconds for the animation to complete | class="my-animation super super-add super-add-active" | - * | 8. The animation ends and all generated CSS classes are removed from the element | class="my-animation super" | - * | 9. The super class is kept on the element | class="my-animation super" | - * | 10. The doneCallback() callback is fired (if provided) | class="my-animation super" | + * | Animation Step | What the element class attribute looks like | + * |----------------------------------------------------------------------------------------------------|------------------------------------------------------------------| + * | 1. $animate.addClass(element, 'super') is called | class="my-animation" | + * | 2. $animate runs the JavaScript-defined animations detected on the element | class="my-animation ng-animate" | + * | 3. the .super-add class is added to the element | class="my-animation ng-animate super-add" | + * | 4. $animate waits for a single animation frame (this performs a reflow) | class="my-animation ng-animate super-add" | + * | 5. the .super and .super-add-active classes are added (this triggers the CSS transition/animation) | class="my-animation ng-animate super super-add super-add-active" | + * | 6. $animate scans the element styles to get the CSS transition/animation duration and delay | class="my-animation ng-animate super-add" | + * | 7. $animate waits for the animation to complete (via events and timeout) | class="my-animation super super-add super-add-active" | + * | 8. The animation ends and all generated CSS classes are removed from the element | class="my-animation super" | + * | 9. The super class is kept on the element | class="my-animation super" | + * | 10. The returned promise is resolved. | class="my-animation super" | * * @param {DOMElement} element the element that will be animated * @param {string} className the CSS class that will be added to the element and then animated - * @param {function()=} doneCallback the callback function that will be called once the animation is complete + * @return {Promise} the animation callback promise */ - addClass : function(element, className, doneCallback) { - element = angular.element(element); - element = stripCommentsFromElement(element); - performAnimation('addClass', className, element, null, null, function() { - $delegate.addClass(element, className); - }, doneCallback); + addClass : function(element, className) { + return this.setClass(element, className, []); }, /** @@ -731,51 +946,106 @@ angular.module('ngAnimate', ['ng']) * * Below is a breakdown of each step that occurs during removeClass animation: * - * | Animation Step | What the element class attribute looks like | - * |-----------------------------------------------------------------------------------------------|---------------------------------------------| - * | 1. $animate.removeClass(element, 'super') is called | class="my-animation super" | - * | 2. $animate runs any JavaScript-defined animations on the element | class="my-animation super ng-animate" | - * | 3. the .super-remove class are added to the element | class="my-animation super ng-animate super-remove"| - * | 4. $animate scans the element styles to get the CSS transition/animation duration and delay | class="my-animation super ng-animate super-remove" | - * | 5. $animate waits for 10ms (this performs a reflow) | class="my-animation super ng-animate super-remove" | - * | 6. the .super-remove-active and .ng-animate-active classes are added and .super is removed (this triggers the CSS transition/animation) | class="my-animation ng-animate ng-animate-active super-remove super-remove-active" | - * | 7. $animate waits for X milliseconds for the animation to complete | class="my-animation ng-animate ng-animate-active super-remove super-remove-active" | - * | 8. The animation ends and all generated CSS classes are removed from the element | class="my-animation" | - * | 9. The doneCallback() callback is fired (if provided) | class="my-animation" | + * | Animation Step | What the element class attribute looks like | + * |------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------| + * | 1. $animate.removeClass(element, 'super') is called | class="my-animation super" | + * | 2. $animate runs the JavaScript-defined animations detected on the element | class="my-animation super ng-animate" | + * | 3. the .super-remove class is added to the element | class="my-animation super ng-animate super-remove" | + * | 4. $animate waits for a single animation frame (this performs a reflow) | class="my-animation super ng-animate super-remove" | + * | 5. the .super-remove-active classes are added and .super is removed (this triggers the CSS transition/animation) | class="my-animation ng-animate super-remove super-remove-active" | + * | 6. $animate scans the element styles to get the CSS transition/animation duration and delay | class="my-animation super ng-animate super-remove" | + * | 7. $animate waits for the animation to complete (via events and timeout) | class="my-animation ng-animate super-remove super-remove-active" | + * | 8. The animation ends and all generated CSS classes are removed from the element | class="my-animation" | + * | 9. The returned promise is resolved. | class="my-animation" | * * * @param {DOMElement} element the element that will be animated * @param {string} className the CSS class that will be animated and then removed from the element - * @param {function()=} doneCallback the callback function that will be called once the animation is complete + * @return {Promise} the animation callback promise */ - removeClass : function(element, className, doneCallback) { - element = angular.element(element); - element = stripCommentsFromElement(element); - performAnimation('removeClass', className, element, null, null, function() { - $delegate.removeClass(element, className); - }, doneCallback); + removeClass : function(element, className) { + return this.setClass(element, [], className); }, - /** - * - * @ngdoc function - * @name $animate#setClass - * @function - * @description Adds and/or removes the given CSS classes to and from the element. - * Once complete, the done() callback will be fired (if provided). - * @param {DOMElement} element the element which will its CSS classes changed - * removed from it - * @param {string} add the CSS classes which will be added to the element - * @param {string} remove the CSS class which will be removed from the element - * @param {Function=} done the callback function (if provided) that will be fired after the - * CSS classes have been set on the element - */ - setClass : function(element, add, remove, doneCallback) { + /** + * + * @ngdoc method + * @name $animate#setClass + * + * @description Adds and/or removes the given CSS classes to and from the element. + * Once complete, the done() callback will be fired (if provided). + * + * | Animation Step | What the element class attribute looks like | + * |--------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------| + * | 1. $animate.removeClass(element, ‘on’, ‘off’) is called | class="my-animation super off” | + * | 2. $animate runs the JavaScript-defined animations detected on the element | class="my-animation super ng-animate off” | + * | 3. the .on-add and .off-remove classes are added to the element | class="my-animation ng-animate on-add off-remove off” | + * | 4. $animate waits for a single animation frame (this performs a reflow) | class="my-animation ng-animate on-add off-remove off” | + * | 5. the .on, .on-add-active and .off-remove-active classes are added and .off is removed (this triggers the CSS transition/animation) | class="my-animation ng-animate on on-add on-add-active off-remove off-remove-active” | + * | 6. $animate scans the element styles to get the CSS transition/animation duration and delay | class="my-animation ng-animate on on-add on-add-active off-remove off-remove-active" | + * | 7. $animate waits for the animation to complete (via events and timeout) | class="my-animation ng-animate on on-add on-add-active off-remove off-remove-active" | + * | 8. The animation ends and all generated CSS classes are removed from the element | class="my-animation on" | + * | 9. The returned promise is resolved. | class="my-animation on" | + * + * @param {DOMElement} element the element which will have its CSS classes changed + * removed from it + * @param {string} add the CSS classes which will be added to the element + * @param {string} remove the CSS class which will be removed from the element + * CSS classes have been set on the element + * @return {Promise} the animation callback promise + */ + setClass : function(element, add, remove) { + var STORAGE_KEY = '$$animateClasses'; element = angular.element(element); element = stripCommentsFromElement(element); - performAnimation('setClass', [add, remove], element, null, null, function() { - $delegate.setClass(element, add, remove); - }, doneCallback); + + if (classBasedAnimationsBlocked(element)) { + return $delegate.setClass(element, add, remove); + } + + add = isArray(add) ? add : add.split(' '); + remove = isArray(remove) ? remove : remove.split(' '); + + var cache = element.data(STORAGE_KEY); + if (cache) { + cache.add = cache.add.concat(add); + cache.remove = cache.remove.concat(remove); + + //the digest cycle will combine all the animations into one function + return cache.promise; + } else { + element.data(STORAGE_KEY, cache = { + add : add, + remove : remove + }); + } + + return cache.promise = runAnimationPostDigest(function(done) { + var cache = element.data(STORAGE_KEY); + element.removeData(STORAGE_KEY); + + var state = element.data(NG_ANIMATE_STATE) || {}; + var classes = resolveElementClasses(element, cache, state.active); + return !classes + ? done() + : performAnimation('setClass', classes, element, null, null, function() { + $delegate.setClass(element, classes[0], classes[1]); + }, done); + }); + }, + + /** + * @ngdoc method + * @name $animate#cancel + * @kind function + * + * @param {Promise} animationPromise The animation promise that is returned when an animation is started. + * + * @description + * Cancels the provided animation. + */ + cancel : function(promise) { + promise.$$cancelFn(); }, /** @@ -794,7 +1064,7 @@ angular.module('ngAnimate', ['ng']) enabled : function(value, element) { switch(arguments.length) { case 2: - if(value) { + if (value) { cleanup(element); } else { var data = element.data(NG_ANIMATE_STATE) || {}; @@ -824,15 +1094,17 @@ angular.module('ngAnimate', ['ng']) */ function performAnimation(animationEvent, className, element, parentElement, afterElement, domOperation, doneCallback) { + var noopCancel = noop; var runner = animationRunner(element, animationEvent, className); - if(!runner) { + if (!runner) { fireDOMOperation(); fireBeforeCallbackAsync(); fireAfterCallbackAsync(); closeAnimation(); - return; + return noopCancel; } + animationEvent = runner.event; className = runner.className; var elementEvents = angular.element._data(runner.node); elementEvents = elementEvents && elementEvents.events; @@ -841,54 +1113,44 @@ angular.module('ngAnimate', ['ng']) parentElement = afterElement ? afterElement.parent() : element.parent(); } - var ngAnimateState = element.data(NG_ANIMATE_STATE) || {}; - var runningAnimations = ngAnimateState.active || {}; - var totalActiveAnimations = ngAnimateState.totalActive || 0; - var lastAnimation = ngAnimateState.last; - - //only allow animations if the currently running animation is not structural - //or if there is no animation running at all - var skipAnimations; - if (runner.isClassBased) { - skipAnimations = ngAnimateState.running || - ngAnimateState.disabled || - (lastAnimation && !lastAnimation.isClassBased); - } - //skip the animation if animations are disabled, a parent is already being animated, //the element is not currently attached to the document body or then completely close //the animation if any matching animations are not found at all. //NOTE: IE8 + IE9 should close properly (run closeAnimation()) in case an animation was found. - if (skipAnimations || animationsDisabled(element, parentElement)) { + if (animationsDisabled(element, parentElement)) { fireDOMOperation(); fireBeforeCallbackAsync(); fireAfterCallbackAsync(); closeAnimation(); - return; + return noopCancel; } + var ngAnimateState = element.data(NG_ANIMATE_STATE) || {}; + var runningAnimations = ngAnimateState.active || {}; + var totalActiveAnimations = ngAnimateState.totalActive || 0; + var lastAnimation = ngAnimateState.last; var skipAnimation = false; - if(totalActiveAnimations > 0) { + + if (totalActiveAnimations > 0) { var animationsToCancel = []; - if(!runner.isClassBased) { - if(animationEvent == 'leave' && runningAnimations['ng-leave']) { + if (!runner.isClassBased) { + if (animationEvent == 'leave' && runningAnimations['ng-leave']) { skipAnimation = true; } else { //cancel all animations when a structural animation takes place for(var klass in runningAnimations) { animationsToCancel.push(runningAnimations[klass]); - cleanup(element, klass); } - runningAnimations = {}; - totalActiveAnimations = 0; + ngAnimateState = {}; + cleanup(element, true); } - } else if(lastAnimation.event == 'setClass') { + } else if (lastAnimation.event == 'setClass') { animationsToCancel.push(lastAnimation); cleanup(element, className); } - else if(runningAnimations[className]) { + else if (runningAnimations[className]) { var current = runningAnimations[className]; - if(current.event == animationEvent) { + if (current.event == animationEvent) { skipAnimation = true; } else { animationsToCancel.push(current); @@ -896,35 +1158,38 @@ angular.module('ngAnimate', ['ng']) } } - if(animationsToCancel.length > 0) { + if (animationsToCancel.length > 0) { forEach(animationsToCancel, function(operation) { operation.cancel(); }); } } - if(runner.isClassBased && !runner.isSetClassOperation && !skipAnimation) { + if (runner.isClassBased && !runner.isSetClassOperation && !skipAnimation) { skipAnimation = (animationEvent == 'addClass') == element.hasClass(className); //opposite of XOR } - if(skipAnimation) { + if (skipAnimation) { fireDOMOperation(); fireBeforeCallbackAsync(); fireAfterCallbackAsync(); fireDoneCallbackAsync(); - return; + return noopCancel; } - if(animationEvent == 'leave') { + runningAnimations = ngAnimateState.active || {}; + totalActiveAnimations = ngAnimateState.totalActive || 0; + + if (animationEvent == 'leave') { //there's no need to ever remove the listener since the element //will be removed (destroyed) after the leave animation ends or //is cancelled midway element.one('$destroy', function(e) { var element = angular.element(this); var state = element.data(NG_ANIMATE_STATE); - if(state) { + if (state) { var activeLeaveAnimation = state.active['ng-leave']; - if(activeLeaveAnimation) { + if (activeLeaveAnimation) { activeLeaveAnimation.cancel(); cleanup(element, 'ng-leave'); } @@ -957,7 +1222,7 @@ angular.module('ngAnimate', ['ng']) (runner.isClassBased && data.active[className].event != animationEvent); fireDOMOperation(); - if(cancelled === true) { + if (cancelled === true) { closeAnimation(); } else { fireAfterCallbackAsync(); @@ -965,9 +1230,11 @@ angular.module('ngAnimate', ['ng']) } }); + return runner.cancel; + function fireDOMCallback(animationPhase) { var eventName = '$animate:' + animationPhase; - if(elementEvents && elementEvents[eventName] && elementEvents[eventName].length > 0) { + if (elementEvents && elementEvents[eventName] && elementEvents[eventName].length > 0) { $$asyncCallback(function() { element.triggerHandler(eventName, { event : animationEvent, @@ -987,37 +1254,33 @@ angular.module('ngAnimate', ['ng']) function fireDoneCallbackAsync() { fireDOMCallback('close'); - if(doneCallback) { - $$asyncCallback(function() { - doneCallback(); - }); - } + doneCallback(); } //it is less complicated to use a flag than managing and canceling //timeouts containing multiple callbacks. function fireDOMOperation() { - if(!fireDOMOperation.hasBeenRun) { + if (!fireDOMOperation.hasBeenRun) { fireDOMOperation.hasBeenRun = true; domOperation(); } } function closeAnimation() { - if(!closeAnimation.hasBeenRun) { + if (!closeAnimation.hasBeenRun) { closeAnimation.hasBeenRun = true; var data = element.data(NG_ANIMATE_STATE); - if(data) { + if (data) { /* only structural animations wait for reflow before removing an animation, but class-based animations don't. An example of this failing would be when a parent HTML tag has a ng-class attribute causing ALL directives below to skip animations during the digest */ - if(runner && runner.isClassBased) { + if (runner && runner.isClassBased) { cleanup(element, className); } else { $$asyncCallback(function() { var data = element.data(NG_ANIMATE_STATE) || {}; - if(localAnimationCount == data.index) { + if (localAnimationCount == data.index) { cleanup(element, className, animationEvent); } }); @@ -1038,7 +1301,7 @@ angular.module('ngAnimate', ['ng']) forEach(nodes, function(element) { element = angular.element(element); var data = element.data(NG_ANIMATE_STATE); - if(data && data.active) { + if (data && data.active) { forEach(data.active, function(runner) { runner.cancel(); }); @@ -1048,21 +1311,21 @@ angular.module('ngAnimate', ['ng']) } function cleanup(element, className) { - if(isMatchingElement(element, $rootElement)) { - if(!rootAnimateState.disabled) { + if (isMatchingElement(element, $rootElement)) { + if (!rootAnimateState.disabled) { rootAnimateState.running = false; rootAnimateState.structural = false; } - } else if(className) { + } else if (className) { var data = element.data(NG_ANIMATE_STATE) || {}; var removeAnimations = className === true; - if(!removeAnimations && data.active && data.active[className]) { + if (!removeAnimations && data.active && data.active[className]) { data.totalActive--; delete data.active[className]; } - if(removeAnimations || !data.totalActive) { + if (removeAnimations || !data.totalActive) { element.removeClass(NG_ANIMATE_CLASS_NAME); element.removeData(NG_ANIMATE_STATE); } @@ -1101,7 +1364,7 @@ angular.module('ngAnimate', ['ng']) //it will be discarded and all child animations will be restricted if (allowChildAnimations !== false) { var animateChildrenFlag = parentElement.data(NG_ANIMATE_CHILDREN); - if(angular.isDefined(animateChildrenFlag)) { + if (angular.isDefined(animateChildrenFlag)) { allowChildAnimations = animateChildrenFlag; } } @@ -1151,9 +1414,9 @@ angular.module('ngAnimate', ['ng']) var PROPERTY_KEY = 'Property'; var DELAY_KEY = 'Delay'; var ANIMATION_ITERATION_COUNT_KEY = 'IterationCount'; + var ANIMATION_PLAYSTATE_KEY = 'PlayState'; var NG_ANIMATE_PARENT_KEY = '$$ngAnimateKey'; var NG_ANIMATE_CSS_DATA_KEY = '$$ngAnimateCSS3Data'; - var NG_ANIMATE_BLOCK_CLASS_NAME = 'ng-animate-block-transitions'; var ELAPSED_TIME_MAX_DECIMAL_PLACES = 3; var CLOSING_TIME_BUFFER = 1.5; var ONE_SECOND = 1000; @@ -1163,7 +1426,7 @@ angular.module('ngAnimate', ['ng']) var animationReflowQueue = []; var cancelAnimationReflow; function afterReflow(element, callback) { - if(cancelAnimationReflow) { + if (cancelAnimationReflow) { cancelAnimationReflow(); } animationReflowQueue.push(callback); @@ -1192,7 +1455,7 @@ angular.module('ngAnimate', ['ng']) //but it may not need to cancel out the existing timeout //if the timestamp is less than the previous one var futureTimestamp = Date.now() + totalTime; - if(futureTimestamp <= closingTimestamp) { + if (futureTimestamp <= closingTimestamp) { return; } @@ -1208,64 +1471,52 @@ angular.module('ngAnimate', ['ng']) function closeAllAnimations(elements) { forEach(elements, function(element) { var elementData = element.data(NG_ANIMATE_CSS_DATA_KEY); - if(elementData) { - (elementData.closeAnimationFn || noop)(); + if (elementData) { + forEach(elementData.closeAnimationFns, function(fn) { + fn(); + }); } }); } function getElementAnimationDetails(element, cacheKey) { var data = cacheKey ? lookupCache[cacheKey] : null; - if(!data) { + if (!data) { var transitionDuration = 0; var transitionDelay = 0; var animationDuration = 0; var animationDelay = 0; - var transitionDelayStyle; - var animationDelayStyle; - var transitionDurationStyle; - var transitionPropertyStyle; //we want all the styles defined before and after forEach(element, function(element) { if (element.nodeType == ELEMENT_NODE) { var elementStyles = $window.getComputedStyle(element) || {}; - transitionDurationStyle = elementStyles[TRANSITION_PROP + DURATION_KEY]; - + var transitionDurationStyle = elementStyles[TRANSITION_PROP + DURATION_KEY]; transitionDuration = Math.max(parseMaxTime(transitionDurationStyle), transitionDuration); - transitionPropertyStyle = elementStyles[TRANSITION_PROP + PROPERTY_KEY]; - - transitionDelayStyle = elementStyles[TRANSITION_PROP + DELAY_KEY]; - + var transitionDelayStyle = elementStyles[TRANSITION_PROP + DELAY_KEY]; transitionDelay = Math.max(parseMaxTime(transitionDelayStyle), transitionDelay); - animationDelayStyle = elementStyles[ANIMATION_PROP + DELAY_KEY]; - - animationDelay = Math.max(parseMaxTime(animationDelayStyle), animationDelay); + var animationDelayStyle = elementStyles[ANIMATION_PROP + DELAY_KEY]; + animationDelay = Math.max(parseMaxTime(elementStyles[ANIMATION_PROP + DELAY_KEY]), animationDelay); var aDuration = parseMaxTime(elementStyles[ANIMATION_PROP + DURATION_KEY]); - if(aDuration > 0) { + if (aDuration > 0) { aDuration *= parseInt(elementStyles[ANIMATION_PROP + ANIMATION_ITERATION_COUNT_KEY], 10) || 1; } - animationDuration = Math.max(aDuration, animationDuration); } }); data = { total : 0, - transitionPropertyStyle: transitionPropertyStyle, - transitionDurationStyle: transitionDurationStyle, - transitionDelayStyle: transitionDelayStyle, transitionDelay: transitionDelay, transitionDuration: transitionDuration, - animationDelayStyle: animationDelayStyle, animationDelay: animationDelay, animationDuration: animationDuration }; - if(cacheKey) { + if (cacheKey) { lookupCache[cacheKey] = data; } } @@ -1286,20 +1537,22 @@ angular.module('ngAnimate', ['ng']) function getCacheKey(element) { var parentElement = element.parent(); var parentID = parentElement.data(NG_ANIMATE_PARENT_KEY); - if(!parentID) { + if (!parentID) { parentElement.data(NG_ANIMATE_PARENT_KEY, ++parentCounter); parentID = parentCounter; } return parentID + '-' + extractElementNode(element).getAttribute('class'); } - function animateSetup(animationEvent, element, className, calculationDecorator) { + function animateSetup(animationEvent, element, className) { + var structural = ['ng-enter','ng-leave','ng-move'].indexOf(className) >= 0; + var cacheKey = getCacheKey(element); var eventCacheKey = cacheKey + ' ' + className; var itemIndex = lookupCache[eventCacheKey] ? ++lookupCache[eventCacheKey].total : 0; var stagger = {}; - if(itemIndex > 0) { + if (itemIndex > 0) { var staggerClassName = className + '-stagger'; var staggerCacheKey = cacheKey + ' ' + staggerClassName; var applyClasses = !lookupCache[staggerCacheKey]; @@ -1311,154 +1564,137 @@ angular.module('ngAnimate', ['ng']) applyClasses && element.removeClass(staggerClassName); } - /* the animation itself may need to add/remove special CSS classes - * before calculating the anmation styles */ - calculationDecorator = calculationDecorator || - function(fn) { return fn(); }; - element.addClass(className); var formerData = element.data(NG_ANIMATE_CSS_DATA_KEY) || {}; - - var timings = calculationDecorator(function() { - return getElementAnimationDetails(element, eventCacheKey); - }); - + var timings = getElementAnimationDetails(element, eventCacheKey); var transitionDuration = timings.transitionDuration; var animationDuration = timings.animationDuration; - if(transitionDuration === 0 && animationDuration === 0) { + + if (structural && transitionDuration === 0 && animationDuration === 0) { element.removeClass(className); return false; } + var blockTransition = structural && transitionDuration > 0; + var blockAnimation = animationDuration > 0 && + stagger.animationDelay > 0 && + stagger.animationDuration === 0; + + var closeAnimationFns = formerData.closeAnimationFns || []; element.data(NG_ANIMATE_CSS_DATA_KEY, { + stagger : stagger, + cacheKey : eventCacheKey, running : formerData.running || 0, itemIndex : itemIndex, - stagger : stagger, - timings : timings, - closeAnimationFn : noop + blockTransition : blockTransition, + closeAnimationFns : closeAnimationFns }); - //temporarily disable the transition so that the enter styles - //don't animate twice (this is here to avoid a bug in Chrome/FF). - var isCurrentlyAnimating = formerData.running > 0 || animationEvent == 'setClass'; - if(transitionDuration > 0) { - blockTransitions(element, className, isCurrentlyAnimating); - } - - //staggering keyframe animations work by adjusting the `animation-delay` CSS property - //on the given element, however, the delay value can only calculated after the reflow - //since by that time $animate knows how many elements are being animated. Therefore, - //until the reflow occurs the element needs to be blocked (where the keyframe animation - //is set to `none 0s`). This blocking mechanism should only be set for when a stagger - //animation is detected and when the element item index is greater than 0. - if(animationDuration > 0 && stagger.animationDelay > 0 && stagger.animationDuration === 0) { - blockKeyframeAnimations(element); - } - - return true; - } - - function isStructuralAnimation(className) { - return className == 'ng-enter' || className == 'ng-move' || className == 'ng-leave'; - } + var node = extractElementNode(element); - function blockTransitions(element, className, isAnimating) { - if(isStructuralAnimation(className) || !isAnimating) { - extractElementNode(element).style[TRANSITION_PROP + PROPERTY_KEY] = 'none'; - } else { - element.addClass(NG_ANIMATE_BLOCK_CLASS_NAME); + if (blockTransition) { + blockTransitions(node, true); } - } - - function blockKeyframeAnimations(element) { - extractElementNode(element).style[ANIMATION_PROP] = 'none 0s'; - } - function unblockTransitions(element, className) { - var prop = TRANSITION_PROP + PROPERTY_KEY; - var node = extractElementNode(element); - if(node.style[prop] && node.style[prop].length > 0) { - node.style[prop] = ''; + if (blockAnimation) { + blockAnimations(node, true); } - element.removeClass(NG_ANIMATE_BLOCK_CLASS_NAME); - } - function unblockKeyframeAnimations(element) { - var prop = ANIMATION_PROP; - var node = extractElementNode(element); - if(node.style[prop] && node.style[prop].length > 0) { - node.style[prop] = ''; - } + return true; } function animateRun(animationEvent, element, className, activeAnimationComplete) { var node = extractElementNode(element); var elementData = element.data(NG_ANIMATE_CSS_DATA_KEY); - if(node.getAttribute('class').indexOf(className) == -1 || !elementData) { + if (node.getAttribute('class').indexOf(className) == -1 || !elementData) { activeAnimationComplete(); return; } + if (elementData.blockTransition) { + blockTransitions(node, false); + } + var activeClassName = ''; + var pendingClassName = ''; forEach(className.split(' '), function(klass, i) { - activeClassName += (i > 0 ? ' ' : '') + klass + '-active'; + var prefix = (i > 0 ? ' ' : '') + klass; + activeClassName += prefix + '-active'; + pendingClassName += prefix + '-pending'; }); - var stagger = elementData.stagger; - var timings = elementData.timings; + var style = ''; + var appliedStyles = []; var itemIndex = elementData.itemIndex; - var maxDuration = Math.max(timings.transitionDuration, timings.animationDuration); - var maxDelay = Math.max(timings.transitionDelay, timings.animationDelay); - var maxDelayTime = maxDelay * ONE_SECOND; - - var startTime = Date.now(); - var css3AnimationEvents = ANIMATIONEND_EVENT + ' ' + TRANSITIONEND_EVENT; + var stagger = elementData.stagger; + var staggerTime = 0; + if (itemIndex > 0) { + var transitionStaggerDelay = 0; + if (stagger.transitionDelay > 0 && stagger.transitionDuration === 0) { + transitionStaggerDelay = stagger.transitionDelay * itemIndex; + } - var style = '', appliedStyles = []; - if(timings.transitionDuration > 0) { - var propertyStyle = timings.transitionPropertyStyle; - if(propertyStyle.indexOf('all') == -1) { - style += CSS_PREFIX + 'transition-property: ' + propertyStyle + ';'; - style += CSS_PREFIX + 'transition-duration: ' + timings.transitionDurationStyle + ';'; - appliedStyles.push(CSS_PREFIX + 'transition-property'); - appliedStyles.push(CSS_PREFIX + 'transition-duration'); + var animationStaggerDelay = 0; + if (stagger.animationDelay > 0 && stagger.animationDuration === 0) { + animationStaggerDelay = stagger.animationDelay * itemIndex; + appliedStyles.push(CSS_PREFIX + 'animation-play-state'); } + + staggerTime = Math.round(Math.max(transitionStaggerDelay, animationStaggerDelay) * 100) / 100; } - if(itemIndex > 0) { - if(stagger.transitionDelay > 0 && stagger.transitionDuration === 0) { - var delayStyle = timings.transitionDelayStyle; - style += CSS_PREFIX + 'transition-delay: ' + - prepareStaggerDelay(delayStyle, stagger.transitionDelay, itemIndex) + '; '; - appliedStyles.push(CSS_PREFIX + 'transition-delay'); - } + if (!staggerTime) { + element.addClass(activeClassName); + } - if(stagger.animationDelay > 0 && stagger.animationDuration === 0) { - style += CSS_PREFIX + 'animation-delay: ' + - prepareStaggerDelay(timings.animationDelayStyle, stagger.animationDelay, itemIndex) + '; '; - appliedStyles.push(CSS_PREFIX + 'animation-delay'); - } + var eventCacheKey = elementData.cacheKey + ' ' + activeClassName; + var timings = getElementAnimationDetails(element, eventCacheKey); + var maxDuration = Math.max(timings.transitionDuration, timings.animationDuration); + if (maxDuration === 0) { + element.removeClass(activeClassName); + animateClose(element, className); + activeAnimationComplete(); + return; } - if(appliedStyles.length > 0) { + var maxDelay = Math.max(timings.transitionDelay, timings.animationDelay); + var maxDelayTime = maxDelay * ONE_SECOND; + + if (appliedStyles.length > 0) { //the element being animated may sometimes contain comment nodes in //the jqLite object, so we're safe to use a single variable to house //the styles since there is always only one element being animated var oldStyle = node.getAttribute('style') || ''; - node.setAttribute('style', oldStyle + '; ' + style); + if (oldStyle.charAt(oldStyle.length-1) !== ';') { + oldStyle += ';'; + } + node.setAttribute('style', oldStyle + ' ' + style); + } + + var startTime = Date.now(); + var css3AnimationEvents = ANIMATIONEND_EVENT + ' ' + TRANSITIONEND_EVENT; + var animationTime = (maxDelay + maxDuration) * CLOSING_TIME_BUFFER; + var totalTime = (staggerTime + animationTime) * ONE_SECOND; + + var staggerTimeout; + if (staggerTime > 0) { + element.addClass(pendingClassName); + staggerTimeout = $timeout(function() { + staggerTimeout = null; + element.addClass(activeClassName); + element.removeClass(pendingClassName); + if (timings.animationDuration > 0) { + blockAnimations(node, false); + } + }, staggerTime * ONE_SECOND, false); } element.on(css3AnimationEvents, onAnimationProgress); - element.addClass(activeClassName); - elementData.closeAnimationFn = function() { + elementData.closeAnimationFns.push(function() { onEnd(); activeAnimationComplete(); - }; - - var staggerTime = itemIndex * (Math.max(stagger.animationDelay, stagger.transitionDelay) || 0); - var animationTime = (maxDelay + maxDuration) * CLOSING_TIME_BUFFER; - var totalTime = (staggerTime + animationTime) * ONE_SECOND; + }); elementData.running++; animationCloseHandler(element, totalTime); @@ -1470,6 +1706,10 @@ angular.module('ngAnimate', ['ng']) function onEnd(cancelled) { element.off(css3AnimationEvents, onAnimationProgress); element.removeClass(activeClassName); + element.removeClass(pendingClassName); + if (staggerTimeout) { + $timeout.cancel(staggerTimeout); + } animateClose(element, className); var node = extractElementNode(element); for (var i in appliedStyles) { @@ -1493,23 +1733,22 @@ angular.module('ngAnimate', ['ng']) * We're checking to see if the timeStamp surpasses the expected delay, * but we're using elapsedTime instead of the timeStamp on the 2nd * pre-condition since animations sometimes close off early */ - if(Math.max(timeStamp - startTime, 0) >= maxDelayTime && elapsedTime >= maxDuration) { + if (Math.max(timeStamp - startTime, 0) >= maxDelayTime && elapsedTime >= maxDuration) { activeAnimationComplete(); } } } - function prepareStaggerDelay(delayStyle, staggerDelay, index) { - var style = ''; - forEach(delayStyle.split(','), function(val, i) { - style += (i > 0 ? ',' : '') + - (index * staggerDelay + parseInt(val, 10)) + 's'; - }); - return style; + function blockTransitions(node, bool) { + node.style[TRANSITION_PROP + PROPERTY_KEY] = bool ? 'none' : ''; + } + + function blockAnimations(node, bool) { + node.style[ANIMATION_PROP + ANIMATION_PLAYSTATE_KEY] = bool ? 'paused' : ''; } function animateBefore(animationEvent, element, className, calculationDecorator) { - if(animateSetup(animationEvent, element, className, calculationDecorator)) { + if (animateSetup(animationEvent, element, className, calculationDecorator)) { return function(cancelled) { cancelled && animateClose(element, className); }; @@ -1517,7 +1756,7 @@ angular.module('ngAnimate', ['ng']) } function animateAfter(animationEvent, element, className, afterAnimationComplete) { - if(element.data(NG_ANIMATE_CSS_DATA_KEY)) { + if (element.data(NG_ANIMATE_CSS_DATA_KEY)) { return animateRun(animationEvent, element, className, afterAnimationComplete); } else { animateClose(element, className); @@ -1530,7 +1769,7 @@ angular.module('ngAnimate', ['ng']) //cancellation function then it means that there is no animation //to perform at all var preReflowCancellation = animateBefore(animationEvent, element, className); - if(!preReflowCancellation) { + if (!preReflowCancellation) { animationComplete(); return; } @@ -1542,8 +1781,6 @@ angular.module('ngAnimate', ['ng']) //happen in the first place var cancel = preReflowCancellation; afterReflow(element, function() { - unblockTransitions(element, className); - unblockKeyframeAnimations(element); //once the reflow is complete then we point cancel to //the new cancellation function which will remove all of the //animation properties from the active animation @@ -1558,11 +1795,11 @@ angular.module('ngAnimate', ['ng']) function animateClose(element, className) { element.removeClass(className); var data = element.data(NG_ANIMATE_CSS_DATA_KEY); - if(data) { - if(data.running) { + if (data) { + if (data.running) { data.running--; } - if(!data.running || data.running === 0) { + if (!data.running || data.running === 0) { element.removeData(NG_ANIMATE_CSS_DATA_KEY); } } @@ -1584,49 +1821,27 @@ angular.module('ngAnimate', ['ng']) beforeSetClass : function(element, add, remove, animationCompleted) { var className = suffixClasses(remove, '-remove') + ' ' + suffixClasses(add, '-add'); - var cancellationMethod = animateBefore('setClass', element, className, function(fn) { - /* when classes are removed from an element then the transition style - * that is applied is the transition defined on the element without the - * CSS class being there. This is how CSS3 functions outside of ngAnimate. - * http://plnkr.co/edit/j8OzgTNxHTb4n3zLyjGW?p=preview */ - var klass = element.attr('class'); - element.removeClass(remove); - element.addClass(add); - var timings = fn(); - element.attr('class', klass); - return timings; - }); - - if(cancellationMethod) { - afterReflow(element, function() { - unblockTransitions(element, className); - unblockKeyframeAnimations(element); - animationCompleted(); - }); + var cancellationMethod = animateBefore('setClass', element, className); + if (cancellationMethod) { + afterReflow(element, animationCompleted); return cancellationMethod; } animationCompleted(); }, beforeAddClass : function(element, className, animationCompleted) { - var cancellationMethod = animateBefore('addClass', element, suffixClasses(className, '-add'), function(fn) { - - /* when a CSS class is added to an element then the transition style that - * is applied is the transition defined on the element when the CSS class - * is added at the time of the animation. This is how CSS3 functions - * outside of ngAnimate. */ - element.addClass(className); - var timings = fn(); - element.removeClass(className); - return timings; - }); + var cancellationMethod = animateBefore('addClass', element, suffixClasses(className, '-add')); + if (cancellationMethod) { + afterReflow(element, animationCompleted); + return cancellationMethod; + } + animationCompleted(); + }, - if(cancellationMethod) { - afterReflow(element, function() { - unblockTransitions(element, className); - unblockKeyframeAnimations(element); - animationCompleted(); - }); + beforeRemoveClass : function(element, className, animationCompleted) { + var cancellationMethod = animateBefore('removeClass', element, suffixClasses(className, '-remove')); + if (cancellationMethod) { + afterReflow(element, animationCompleted); return cancellationMethod; } animationCompleted(); @@ -1643,30 +1858,6 @@ angular.module('ngAnimate', ['ng']) return animateAfter('addClass', element, suffixClasses(className, '-add'), animationCompleted); }, - beforeRemoveClass : function(element, className, animationCompleted) { - var cancellationMethod = animateBefore('removeClass', element, suffixClasses(className, '-remove'), function(fn) { - /* when classes are removed from an element then the transition style - * that is applied is the transition defined on the element without the - * CSS class being there. This is how CSS3 functions outside of ngAnimate. - * http://plnkr.co/edit/j8OzgTNxHTb4n3zLyjGW?p=preview */ - var klass = element.attr('class'); - element.removeClass(className); - var timings = fn(); - element.attr('class', klass); - return timings; - }); - - if(cancellationMethod) { - afterReflow(element, function() { - unblockTransitions(element, className); - unblockKeyframeAnimations(element); - animationCompleted(); - }); - return cancellationMethod; - } - animationCompleted(); - }, - removeClass : function(element, className, animationCompleted) { return animateAfter('removeClass', element, suffixClasses(className, '-remove'), animationCompleted); } @@ -1674,9 +1865,9 @@ angular.module('ngAnimate', ['ng']) function suffixClasses(classes, suffix) { var className = ''; - classes = angular.isArray(classes) ? classes : classes.split(/\s+/); + classes = isArray(classes) ? classes : classes.split(/\s+/); forEach(classes, function(klass, i) { - if(klass && klass.length > 0) { + if (klass && klass.length > 0) { className += (i > 0 ? ' ' : '') + klass + suffix; } }); diff --git a/webclient/js/angular-animate.min.js b/webclient/js/angular-animate.min.js index 0128b6e468..1ce2a93ac7 100644 --- a/webclient/js/angular-animate.min.js +++ b/webclient/js/angular-animate.min.js @@ -1,21 +1,30 @@ /* - AngularJS v1.2.0 - (c) 2010-2012 Google, Inc. http://angularjs.org + AngularJS v1.3.0-rc.1 + (c) 2010-2014 Google, Inc. http://angularjs.org License: MIT */ -(function(A,s,B){'use strict';s.module("ngAnimate",["ng"]).config(["$provide","$animateProvider",function(N,D){var z=s.noop,k=s.forEach,X=D.$$selectors,T=1,f="$$ngAnimateState",E="ng-animate",g={running:!0};N.decorator("$animate",["$delegate","$injector","$sniffer","$rootElement","$timeout","$rootScope","$document",function(t,A,F,m,G,p,H){function B(a){if(a){var d=[],b={};a=a.substr(1).split(".");(F.transitions||F.animations)&&a.push("");for(var c=0;c<a.length;c++){var h=a[c],f=X[h];f&&!b[h]&&(d.push(A.get(f)), -b[h]=!0)}return d}}function n(a,d,b,c,h,g,m){function p(a){g();if(!0===a)u();else{if(a=b.data(f))a.done=u,b.data(f,a);s(v,"after",u)}}function s(c,h,g){var f=h+"End";k(c,function(k,t){var e=function(){a:{var e=h+"Complete",a=c[t];a[e]=!0;(a[f]||z)();for(a=0;a<c.length;a++)if(!c[a][e])break a;g()}};"before"!=h||"enter"!=a&&"move"!=a?k[h]?k[f]=n?k[h](b,d,e):k[h](b,e):e():e()})}function t(){m&&G(m,0,!1)}function u(){if(!u.hasBeenRun){u.hasBeenRun=!0;var a=b.data(f);a&&(n?q(b):(a.closeAnimationTimeout= -G(function(){q(b)},0,!1),b.data(f,a)));t()}}var r=(" "+((b.attr("class")||"")+" "+d)).replace(/\s+/g,".");c||(c=h?h.parent():b.parent());h=B(r);var n="addClass"==a||"removeClass"==a,r=b.data(f)||{};if(J(b,c)||0===h.length)g(),u();else{var v=[];r.running&&n&&r.structural||k(h,function(c){if(!c.allowCancel||c.allowCancel(b,a,d)){var h=c[a];"leave"==a?(c=h,h=null):c=c["before"+a.charAt(0).toUpperCase()+a.substr(1)];v.push({before:c,after:h})}});0===v.length?(g(),t()):(r.running&&(G.cancel(r.closeAnimationTimeout), -q(b),K(r.animations),(r.done||z)(!0)),"addClass"==a&&b.hasClass(d)||"removeClass"==a&&!b.hasClass(d)?(g(),t()):(b.addClass(E),b.data(f,{running:!0,structural:!n,animations:v,done:p}),s(v,"before",p)))}}function L(a){a=a[0];a.nodeType==T&&k(a.querySelectorAll("."+E),function(a){a=s.element(a);var b=a.data(f);b&&(K(b.animations),q(a))})}function K(a){k(a,function(d){a.beforeComplete||(d.beforeEnd||z)(!0);a.afterComplete||(d.afterEnd||z)(!0)})}function q(a){a[0]==m[0]?g.disabled||(g.running=!1,g.structural= -!1):(a.removeClass(E),a.removeData(f))}function J(a,d){if(g.disabled)return!0;if(a[0]==m[0])return g.disabled||g.running;do{if(0===d.length)break;var b=d[0]==m[0],c=b?g:d.data(f),c=c&&(!!c.disabled||!!c.running);if(b||c)return c;if(b)break}while(d=d.parent());return!0}m.data(f,g);p.$$postDigest(function(){g.running=!1});return{enter:function(a,d,b,c){this.enabled(!1,a);t.enter(a,d,b);p.$$postDigest(function(){n("enter","ng-enter",a,d,b,z,c)})},leave:function(a,d){L(a);this.enabled(!1,a);p.$$postDigest(function(){n("leave", -"ng-leave",a,null,null,function(){t.leave(a)},d)})},move:function(a,d,b,c){L(a);this.enabled(!1,a);t.move(a,d,b);p.$$postDigest(function(){n("move","ng-move",a,d,b,z,c)})},addClass:function(a,d,b){n("addClass",d,a,null,null,function(){t.addClass(a,d)},b)},removeClass:function(a,d,b){n("removeClass",d,a,null,null,function(){t.removeClass(a,d)},b)},enabled:function(a,d){switch(arguments.length){case 2:if(a)q(d);else{var b=d.data(f)||{};b.disabled=!0;d.data(f,b)}break;case 1:g.disabled=!a;break;default:a= -!g.disabled}return!!a}}}]);D.register("",["$window","$sniffer","$timeout",function(f,g,F){function m(e){Q.push(e);F.cancel(R);R=F(function(){k(Q,function(e){e()});Q=[];R=null;C={}},10,!1)}function G(e,a){var w=e.getAttribute("style")||"";e.setAttribute("style",(0<w.length?"; ":"")+a);return w}function p(e,a){var w=a?C[a]:null;if(!w){var b=0,d=0,c=0,l=0,g,n,m,p;k(e,function(e){if(e.nodeType==T){e=f.getComputedStyle(e)||{};m=e[h+D];b=Math.max(H(m),b);p=e[h+S];g=e[h+u];d=Math.max(H(g),d);n=e[I+u];l= -Math.max(H(n),l);var a=H(e[I+D]);0<a&&(a*=parseInt(e[I+r],10)||1);c=Math.max(a,c)}});w={total:0,transitionPropertyStyle:p,transitionDurationStyle:m,transitionDelayStyle:g,transitionDelay:d,transitionDuration:b,animationDelayStyle:n,animationDelay:l,animationDuration:c};a&&(C[a]=w)}return w}function H(e){var a=0;e=s.isString(e)?e.split(/\s*,\s*/):[];k(e,function(e){a=Math.max(parseFloat(e)||0,a)});return a}function E(e){var a=e.parent(),b=a.data(W);b||(a.data(W,++V),b=V);return b+"-"+e[0].className} -function n(e,a){var b=E(e),d=b+" "+a,c={},g=C[d]?++C[d].total:0;if(0<g){var l=a+"-stagger",c=b+" "+l;(b=!C[c])&&e.addClass(l);c=p(e,c);b&&e.removeClass(l)}e.addClass(a);d=p(e,d);l=Math.max(d.transitionDuration,d.animationDuration);if(0===l)return e.removeClass(a),!1;var b=e[0],f="";0<d.transitionDuration&&(e.addClass(U),f+=N+" ",b.style[h+S]="none");k(a.split(" "),function(a,e){f+=(0<e?" ":"")+a+"-active"});e.data(v,{className:a,activeClassName:f,maxDuration:l,classes:a+" "+f,timings:d,stagger:c, -ii:g});return!0}function L(a,b,w){function f(a){a.stopPropagation();a=a.originalEvent||a;var e=a.$manualTimeStamp||a.timeStamp||Date.now();Math.max(e-t,0)>=p&&a.elapsedTime>=n&&w()}var x=a.data(v);if(a.hasClass(b)&&x){var M=a[0],l=x.timings,k=x.stagger,n=x.maxDuration,m=x.activeClassName,p=1E3*Math.max(l.transitionDelay,l.animationDelay),t=Date.now(),s=P+" "+O,r,x=x.ii,u,y="";if(0<l.transitionDuration){M.style[h+S]="";var q=l.transitionPropertyStyle;-1==q.indexOf("all")&&(u=!0,y+=c+"transition-property: "+ -q+", "+(g.msie?"-ms-zoom":"clip")+"; ",y+=c+"transition-duration: "+l.transitionDurationStyle+", "+l.transitionDuration+"s; ")}0<x&&(0<k.transitionDelay&&0===k.transitionDuration&&(q=l.transitionDelayStyle,u&&(q+=", "+l.transitionDelay+"s"),y+=c+"transition-delay: "+K(q,k.transitionDelay,x)+"; "),0<k.animationDelay&&0===k.animationDuration&&(y+=c+"animation-delay: "+K(l.animationDelayStyle,k.animationDelay,x)+"; "));0<y.length&&(r=G(M,y));a.on(s,f);a.addClass(m);return function(c){a.off(s,f);a.removeClass(m); -d(a,b);null!=r&&(0<r.length?M.setAttribute("style",r):M.removeAttribute("style"))}}w()}function K(a,b,d){var c="";k(a.split(","),function(a,e){c+=(0<e?",":"")+(d*b+parseInt(a,10))+"s"});return c}function q(a,b){if(n(a,b))return function(c){c&&d(a,b)}}function J(a,b,c){if(a.data(v))return L(a,b,c);d(a,b);c()}function a(a,b,c){var d=q(a,b);if(d){var f=d;m(function(){f=J(a,b,c)});return function(a){(f||z)(a)}}c()}function d(a,b){a.removeClass(b);a.removeClass(U);a.removeData(v)}function b(a,b){var c= -"";a=s.isArray(a)?a:a.split(/\s+/);k(a,function(a,e){a&&0<a.length&&(c+=(0<e?" ":"")+a+b)});return c}var c="",h,O,I,P;A.ontransitionend===B&&A.onwebkittransitionend!==B?(c="-webkit-",h="WebkitTransition",O="webkitTransitionEnd transitionend"):(h="transition",O="transitionend");A.onanimationend===B&&A.onwebkitanimationend!==B?(c="-webkit-",I="WebkitAnimation",P="webkitAnimationEnd animationend"):(I="animation",P="animationend");var D="Duration",S="Property",u="Delay",r="IterationCount",W="$$ngAnimateKey", -v="$$ngAnimateCSS3Data",U="ng-animate-start",N="ng-animate-active",C={},V=0,Q=[],R;return{allowCancel:function(a,c,d){var f=(a.data(v)||{}).classes;if(!f||0<=["enter","leave","move"].indexOf(c))return!0;var h=a.parent(),g=s.element(a[0].cloneNode());g.attr("style","position:absolute; top:-9999px; left:-9999px");g.removeAttr("id");g.html("");k(f.split(" "),function(a){g.removeClass(a)});g.addClass(b(d,"addClass"==c?"-add":"-remove"));h.append(g);a=p(g);g.remove();return 0<Math.max(a.transitionDuration, -a.animationDuration)},enter:function(b,c){return a(b,"ng-enter",c)},leave:function(b,c){return a(b,"ng-leave",c)},move:function(b,c){return a(b,"ng-move",c)},beforeAddClass:function(a,c,d){if(a=q(a,b(c,"-add")))return m(d),a;d()},addClass:function(a,c,d){return J(a,b(c,"-add"),d)},beforeRemoveClass:function(a,c,d){if(a=q(a,b(c,"-remove")))return m(d),a;d()},removeClass:function(a,c,d){return J(a,b(c,"-remove"),d)}}}])}])})(window,window.angular); +(function(D,f,E){'use strict';f.module("ngAnimate",["ng"]).directive("ngAnimateChildren",function(){return function(P,w,g){g=g.ngAnimateChildren;f.isString(g)&&0===g.length?w.data("$$ngAnimateChildren",!0):P.$watch(g,function(f){w.data("$$ngAnimateChildren",!!f)})}}).factory("$$animateReflow",["$$rAF","$document",function(f,w){return function(g){return f(function(){g()})}}]).config(["$provide","$animateProvider",function(P,w){function g(f){for(var g=0;g<f.length;g++){var h=f[g];if(1==h.nodeType)return h}} +function Z(f,h){return g(f)==g(h)}var q=f.noop,h=f.forEach,$=w.$$selectors,W=f.isArray,u={running:!0};P.decorator("$animate",["$delegate","$$q","$injector","$sniffer","$rootElement","$$asyncCallback","$rootScope","$document","$templateRequest",function(Q,D,I,E,v,X,x,Y,J){function K(a,c){var d=a.data("$$ngAnimateState")||{};c&&(d.running=!0,d.structural=!0,a.data("$$ngAnimateState",d));return d.disabled||d.running&&d.structural}function L(a){var c,d=D.defer();d.promise.$$cancelFn=function(){c&&c()}; +x.$$postDigest(function(){c=a(function(){d.resolve()})});return d.promise}function M(a,c,d){d=d||{};var e={};h(c.add,function(b){b&&b.length&&(e[b]=e[b]||0,e[b]++)});h(c.remove,function(b){b&&b.length&&(e[b]=e[b]||0,e[b]--)});var R=[];h(d,function(b,a){h(a.split(" "),function(a){R[a]=b})});var g=[],l=[];h(e,function(b,c){var d=f.$$hasClass(a[0],c),e=R[c]||{};0>b?(d||"addClass"==e.event)&&l.push(c):0<b&&(d&&"removeClass"!=e.event||g.push(c))});return 0<g.length+l.length&&[g.join(" "),l.join(" ")]} +function N(a){if(a){var c=[],d={};a=a.substr(1).split(".");(E.transitions||E.animations)&&c.push(I.get($[""]));for(var e=0;e<a.length;e++){var f=a[e],g=$[f];g&&!d[f]&&(c.push(I.get(g)),d[f]=!0)}return c}}function T(a,c,d){function e(b,a){var c=b[a],d=b["before"+a.charAt(0).toUpperCase()+a.substr(1)];if(c||d)return"leave"==a&&(d=c,c=null),U.push({event:a,fn:c}),m.push({event:a,fn:d}),!0}function f(c,r,e){var A=[];h(c,function(b){b.fn&&A.push(b)});var k=0;h(A,function(c,f){var g=function(){a:{if(r){(r[f]|| +q)();if(++k<A.length)break a;r=null}e()}};switch(c.event){case "setClass":r.push(c.fn(a,l,b,g));break;case "addClass":r.push(c.fn(a,l||d,g));break;case "removeClass":r.push(c.fn(a,b||d,g));break;default:r.push(c.fn(a,g))}});r&&0===r.length&&e()}var g=a[0];if(g){var l,b;W(d)&&(l=d[0],b=d[1],l?b?d=l+" "+b:(d=l,c="addClass"):(d=b,c="removeClass"));var r="setClass"==c,A=r||"addClass"==c||"removeClass"==c,y=a.attr("class")+" "+d;if(B(y)){var C=q,k=[],m=[],n=q,s=[],U=[],y=(" "+y).replace(/\s+/g,".");h(N(y), +function(b){!e(b,c)&&r&&(e(b,"addClass"),e(b,"removeClass"))});return{node:g,event:c,className:d,isClassBased:A,isSetClassOperation:r,before:function(b){C=b;f(m,k,function(){C=q;b()})},after:function(b){n=b;f(U,s,function(){n=q;b()})},cancel:function(){k&&(h(k,function(b){(b||q)(!0)}),C(!0));s&&(h(s,function(b){(b||q)(!0)}),n(!0))}}}}}function F(a,c,d,e,g,S,l){function b(b){var l="$animate:"+b;m&&m[l]&&0<m[l].length&&X(function(){d.triggerHandler(l,{event:a,className:c})})}function r(){b("before")} +function A(){b("after")}function y(){y.hasBeenRun||(y.hasBeenRun=!0,S())}function C(){if(!C.hasBeenRun){C.hasBeenRun=!0;var r=d.data("$$ngAnimateState");r&&(k&&k.isClassBased?z(d,c):(X(function(){var b=d.data("$$ngAnimateState")||{};p==b.index&&z(d,c,a)}),d.data("$$ngAnimateState",r)));b("close");l()}}var k=T(d,a,c);if(!k)return y(),r(),A(),C(),q;a=k.event;c=k.className;var m=f.element._data(k.node),m=m&&m.events;e||(e=g?g.parent():d.parent());if(H(d,e))return y(),r(),A(),C(),q;e=d.data("$$ngAnimateState")|| +{};var n=e.active||{},s=e.totalActive||0,U=e.last;g=!1;if(0<s){s=[];if(k.isClassBased)"setClass"==U.event?(s.push(U),z(d,c)):n[c]&&(t=n[c],t.event==a?g=!0:(s.push(t),z(d,c)));else if("leave"==a&&n["ng-leave"])g=!0;else{for(var t in n)s.push(n[t]);e={};z(d,!0)}0<s.length&&h(s,function(b){b.cancel()})}!k.isClassBased||k.isSetClassOperation||g||(g="addClass"==a==d.hasClass(c));if(g)return y(),r(),A(),b("close"),l(),q;n=e.active||{};s=e.totalActive||0;if("leave"==a)d.one("$destroy",function(b){b=f.element(this); +var a=b.data("$$ngAnimateState");a&&(a=a.active["ng-leave"])&&(a.cancel(),z(b,"ng-leave"))});d.addClass("ng-animate");var p=G++;s++;n[c]=k;d.data("$$ngAnimateState",{last:k,active:n,index:p,totalActive:s});r();k.before(function(b){var l=d.data("$$ngAnimateState");b=b||!l||!l.active[c]||k.isClassBased&&l.active[c].event!=a;y();!0===b?C():(A(),k.after(C))});return k.cancel}function p(a){if(a=g(a))a=f.isFunction(a.getElementsByClassName)?a.getElementsByClassName("ng-animate"):a.querySelectorAll(".ng-animate"), +h(a,function(a){a=f.element(a);(a=a.data("$$ngAnimateState"))&&a.active&&h(a.active,function(a){a.cancel()})})}function z(a,c){if(Z(a,v))u.disabled||(u.running=!1,u.structural=!1);else if(c){var d=a.data("$$ngAnimateState")||{},e=!0===c;!e&&d.active&&d.active[c]&&(d.totalActive--,delete d.active[c]);if(e||!d.totalActive)a.removeClass("ng-animate"),a.removeData("$$ngAnimateState")}}function H(a,c){if(u.disabled)return!0;if(Z(a,v))return u.running;var d,e,g;do{if(0===c.length)break;var h=Z(c,v),l=h? +u:c.data("$$ngAnimateState")||{};if(l.disabled)return!0;h&&(g=!0);!1!==d&&(h=c.data("$$ngAnimateChildren"),f.isDefined(h)&&(d=h));e=e||l.running||l.last&&!l.last.isClassBased}while(c=c.parent());return!g||!d&&e}v.data("$$ngAnimateState",u);var V=x.$watch(function(){return J.totalPendingRequests},function(a,c){0===a&&(V(),x.$$postDigest(function(){x.$$postDigest(function(){u.running=!1})}))}),G=0,O=w.classNameFilter(),B=O?function(a){return O.test(a)}:function(){return!0};return{enter:function(a,c, +d){a=f.element(a);c=c&&f.element(c);d=d&&f.element(d);K(a,!0);Q.enter(a,c,d);return L(function(e){return F("enter","ng-enter",f.element(g(a)),c,d,q,e)})},leave:function(a){a=f.element(a);p(a);K(a,!0);this.enabled(!1,a);return L(function(c){return F("leave","ng-leave",f.element(g(a)),null,null,function(){Q.leave(a)},c)})},move:function(a,c,d){a=f.element(a);c=c&&f.element(c);d=d&&f.element(d);p(a);K(a,!0);Q.move(a,c,d);return L(function(e){return F("move","ng-move",f.element(g(a)),c,d,q,e)})},addClass:function(a, +c){return this.setClass(a,c,[])},removeClass:function(a,c){return this.setClass(a,[],c)},setClass:function(a,c,d){a=f.element(a);a=f.element(g(a));if(K(a))return Q.setClass(a,c,d);c=W(c)?c:c.split(" ");d=W(d)?d:d.split(" ");var e=a.data("$$animateClasses");if(e)return e.add=e.add.concat(c),e.remove=e.remove.concat(d),e.promise;a.data("$$animateClasses",e={add:c,remove:d});return e.promise=L(function(c){var d=a.data("$$animateClasses");a.removeData("$$animateClasses");var l=a.data("$$ngAnimateState")|| +{},b=M(a,d,l.active);return b?F("setClass",b,a,null,null,function(){Q.setClass(a,b[0],b[1])},c):c()})},cancel:function(a){a.$$cancelFn()},enabled:function(a,c){switch(arguments.length){case 2:if(a)z(c);else{var d=c.data("$$ngAnimateState")||{};d.disabled=!0;c.data("$$ngAnimateState",d)}break;case 1:u.disabled=!a;break;default:a=!u.disabled}return!!a}}}]);w.register("",["$window","$sniffer","$timeout","$$animateReflow",function(u,w,I,P){function v(a,b){d&&d();c.push(b);d=P(function(){h(c,function(b){b()}); +c=[];d=null;B={}})}function X(a,b){var c=g(a);a=f.element(c);S.push(a);c=Date.now()+b;c<=R||(I.cancel(e),R=c,e=I(function(){x(S);S=[]},b,!1))}function x(a){h(a,function(b){(b=b.data("$$ngAnimateCSS3Data"))&&h(b.closeAnimationFns,function(b){b()})})}function Y(a,b){var c=b?B[b]:null;if(!c){var d=0,e=0,g=0,f=0;h(a,function(b){if(1==b.nodeType){b=u.getComputedStyle(b)||{};d=Math.max(J(b[H+"Duration"]),d);e=Math.max(J(b[H+"Delay"]),e);f=Math.max(J(b[G+"Delay"]),f);var a=J(b[G+"Duration"]);0<a&&(a*=parseInt(b[G+ +"IterationCount"],10)||1);g=Math.max(a,g)}});c={total:0,transitionDelay:e,transitionDuration:d,animationDelay:f,animationDuration:g};b&&(B[b]=c)}return c}function J(a){var b=0;a=f.isString(a)?a.split(/\s*,\s*/):[];h(a,function(a){b=Math.max(parseFloat(a)||0,b)});return b}function K(c,b,d){c=0<=["ng-enter","ng-leave","ng-move"].indexOf(d);var e,f=b.parent(),h=f.data("$$ngAnimateKey");h||(f.data("$$ngAnimateKey",++a),h=a);e=h+"-"+g(b).getAttribute("class");var f=e+" "+d,h=B[f]?++B[f].total:0,k={};if(0< +h){var m=d+"-stagger",k=e+" "+m;(e=!B[k])&&b.addClass(m);k=Y(b,k);e&&b.removeClass(m)}b.addClass(d);var m=b.data("$$ngAnimateCSS3Data")||{},n=Y(b,f);e=n.transitionDuration;n=n.animationDuration;if(c&&0===e&&0===n)return b.removeClass(d),!1;d=c&&0<e;c=0<n&&0<k.animationDelay&&0===k.animationDuration;b.data("$$ngAnimateCSS3Data",{stagger:k,cacheKey:f,running:m.running||0,itemIndex:h,blockTransition:d,closeAnimationFns:m.closeAnimationFns||[]});b=g(b);d&&(b.style[H+"Property"]="none");c&&(b.style[G+ +"PlayState"]="paused");return!0}function L(a,b,c,d){function e(a){b.off(D,f);b.removeClass(m);b.removeClass(n);x&&I.cancel(x);F(b,c);a=g(b);for(var d in s)a.style.removeProperty(s[d])}function f(b){b.stopPropagation();var a=b.originalEvent||b;b=a.$manualTimeStamp||a.timeStamp||Date.now();a=parseFloat(a.elapsedTime.toFixed(3));Math.max(b-E,0)>=B&&a>=w&&d()}var k=g(b);a=b.data("$$ngAnimateCSS3Data");if(-1!=k.getAttribute("class").indexOf(c)&&a){a.blockTransition&&(k.style[H+"Property"]="");var m="", +n="";h(c.split(" "),function(b,a){var c=(0<a?" ":"")+b;m+=c+"-active";n+=c+"-pending"});var s=[],p=a.itemIndex,t=a.stagger,q=0;if(0<p){q=0;0<t.transitionDelay&&0===t.transitionDuration&&(q=t.transitionDelay*p);var u=0;0<t.animationDelay&&0===t.animationDuration&&(u=t.animationDelay*p,s.push(z+"animation-play-state"));q=Math.round(100*Math.max(q,u))/100}q||b.addClass(m);var v=Y(b,a.cacheKey+" "+m),w=Math.max(v.transitionDuration,v.animationDuration);if(0===w)b.removeClass(m),F(b,c),d();else{var p= +Math.max(v.transitionDelay,v.animationDelay),B=1E3*p;0<s.length&&(t=k.getAttribute("style")||"",";"!==t.charAt(t.length-1)&&(t+=";"),k.setAttribute("style",t+" "));var E=Date.now(),D=O+" "+V,p=1E3*(q+1.5*(p+w)),x;0<q&&(b.addClass(n),x=I(function(){x=null;b.addClass(m);b.removeClass(n);0<v.animationDuration&&(k.style[G+"PlayState"]="")},1E3*q,!1));b.on(D,f);a.closeAnimationFns.push(function(){e();d()});a.running++;X(b,p);return e}}else d()}function M(a,b,c,d){if(K(a,b,c,d))return function(a){a&&F(b, +c)}}function N(a,b,c,d){if(b.data("$$ngAnimateCSS3Data"))return L(a,b,c,d);F(b,c);d()}function T(a,b,c,d){var e=M(a,b,c);if(e){var f=e;v(b,function(){f=N(a,b,c,d)});return function(b){(f||q)(b)}}d()}function F(a,b){a.removeClass(b);var c=a.data("$$ngAnimateCSS3Data");c&&(c.running&&c.running--,c.running&&0!==c.running||a.removeData("$$ngAnimateCSS3Data"))}function p(a,b){var c="";a=W(a)?a:a.split(/\s+/);h(a,function(a,d){a&&0<a.length&&(c+=(0<d?" ":"")+a+b)});return c}var z="",H,V,G,O;D.ontransitionend=== +E&&D.onwebkittransitionend!==E?(z="-webkit-",H="WebkitTransition",V="webkitTransitionEnd transitionend"):(H="transition",V="transitionend");D.onanimationend===E&&D.onwebkitanimationend!==E?(z="-webkit-",G="WebkitAnimation",O="webkitAnimationEnd animationend"):(G="animation",O="animationend");var B={},a=0,c=[],d,e=null,R=0,S=[];return{enter:function(a,b){return T("enter",a,"ng-enter",b)},leave:function(a,b){return T("leave",a,"ng-leave",b)},move:function(a,b){return T("move",a,"ng-move",b)},beforeSetClass:function(a, +b,c,d){b=p(c,"-remove")+" "+p(b,"-add");if(b=M("setClass",a,b))return v(a,d),b;d()},beforeAddClass:function(a,b,c){if(b=M("addClass",a,p(b,"-add")))return v(a,c),b;c()},beforeRemoveClass:function(a,b,c){if(b=M("removeClass",a,p(b,"-remove")))return v(a,c),b;c()},setClass:function(a,b,c,d){c=p(c,"-remove");b=p(b,"-add");return N("setClass",a,c+" "+b,d)},addClass:function(a,b,c){return N("addClass",a,p(b,"-add"),c)},removeClass:function(a,b,c){return N("removeClass",a,p(b,"-remove"),c)}}}])}])})(window, +window.angular); //# sourceMappingURL=angular-animate.min.js.map diff --git a/webclient/js/angular-route.js b/webclient/js/angular-route.js index c4da03a645..305d92e855 100644 --- a/webclient/js/angular-route.js +++ b/webclient/js/angular-route.js @@ -1,5 +1,5 @@ /** - * @license AngularJS v1.2.20 + * @license AngularJS v1.3.0-rc.1 * (c) 2010-2014 Google, Inc. http://angularjs.org * License: MIT */ @@ -22,12 +22,12 @@ */ /* global -ngRouteModule */ var ngRouteModule = angular.module('ngRoute', ['ng']). - provider('$route', $RouteProvider); + provider('$route', $RouteProvider), + $routeMinErr = angular.$$minErr('ngRoute'); /** * @ngdoc provider * @name $routeProvider - * @kind function * * @description * @@ -216,10 +216,14 @@ function $RouteProvider(){ * Sets route definition that will be used on route change when no other route definition * is matched. * - * @param {Object} params Mapping information to be assigned to `$route.current`. + * @param {Object|string} params Mapping information to be assigned to `$route.current`. + * If called with a string, the value maps to `redirectTo`. * @returns {Object} self */ this.otherwise = function(params) { + if (typeof params === 'string') { + params = {redirectTo: params}; + } this.when(null, params); return this; }; @@ -230,10 +234,9 @@ function $RouteProvider(){ '$routeParams', '$q', '$injector', - '$http', - '$templateCache', + '$templateRequest', '$sce', - function($rootScope, $location, $routeParams, $q, $injector, $http, $templateCache, $sce) { + function($rootScope, $location, $routeParams, $q, $injector, $templateRequest, $sce) { /** * @ngdoc service @@ -441,6 +444,36 @@ function $RouteProvider(){ reload: function() { forceReload = true; $rootScope.$evalAsync(updateRoute); + }, + + /** + * @ngdoc method + * @name $route#updateParams + * + * @description + * Causes `$route` service to update the current URL, replacing + * current route parameters with those specified in `newParams`. + * Provided property names that match the route's path segment + * definitions will be interpolated into the location's path, while + * remaining properties will be treated as query params. + * + * @param {Object} newParams mapping of URL parameter names to values + */ + updateParams: function(newParams) { + if (this.current && this.current.$$route) { + var searchParams = {}, self=this; + + angular.forEach(Object.keys(newParams), function(key) { + if (!self.current.pathParams[key]) searchParams[key] = newParams[key]; + }); + + newParams = angular.extend({}, this.current.params, newParams); + $location.path(interpolate(this.current.$$route.originalPath, newParams)); + $location.search(angular.extend({}, $location.search(), searchParams)); + } + else { + throw $routeMinErr('norout', 'Tried updating route when with no current route'); + } } }; @@ -473,9 +506,7 @@ function $RouteProvider(){ for (var i = 1, len = m.length; i < len; ++i) { var key = keys[i - 1]; - var val = 'string' == typeof m[i] - ? decodeURIComponent(m[i]) - : m[i]; + var val = m[i]; if (key && val) { params[key.name] = val; @@ -518,7 +549,7 @@ function $RouteProvider(){ angular.forEach(locals, function(value, key) { locals[key] = angular.isString(value) ? - $injector.get(value) : $injector.invoke(value); + $injector.get(value) : $injector.invoke(value, null, null, key); }); if (angular.isDefined(template = next.template)) { @@ -532,8 +563,7 @@ function $RouteProvider(){ templateUrl = $sce.getTrustedResourceUrl(templateUrl); if (angular.isDefined(templateUrl)) { next.loadedTemplateUrl = templateUrl; - template = $http.get(templateUrl, {cache: $templateCache}). - then(function(response) { return response.data; }); + template = $templateRequest(templateUrl); } } if (angular.isDefined(template)) { @@ -695,7 +725,6 @@ ngRouteModule.directive('ngView', ngViewFillContentFactory); <pre>$location.path() = {{main.$location.path()}}</pre> <pre>$route.current.templateUrl = {{main.$route.current.templateUrl}}</pre> <pre>$route.current.params = {{main.$route.current.params}}</pre> - <pre>$route.current.scope.name = {{main.$route.current.scope.name}}</pre> <pre>$routeParams = {{main.$routeParams}}</pre> </div> </file> @@ -846,7 +875,7 @@ function ngViewFactory( $route, $anchorScroll, $animate) { currentScope = null; } if(currentElement) { - $animate.leave(currentElement, function() { + $animate.leave(currentElement).then(function() { previousElement = null; }); previousElement = currentElement; @@ -869,7 +898,7 @@ function ngViewFactory( $route, $anchorScroll, $animate) { // function is called before linking the content, which would apply child // directives to non existing elements. var clone = $transclude(newScope, function(clone) { - $animate.enter(clone, null, currentElement || $element, function onNgViewEnter () { + $animate.enter(clone, null, currentElement || $element).then(function onNgViewEnter () { if (angular.isDefined(autoScrollExp) && (!autoScrollExp || scope.$eval(autoScrollExp))) { $anchorScroll(); diff --git a/webclient/js/angular-route.min.js b/webclient/js/angular-route.min.js index efdc1a2151..03da279ec3 100644 --- a/webclient/js/angular-route.min.js +++ b/webclient/js/angular-route.min.js @@ -1,14 +1,14 @@ /* - AngularJS v1.2.0 - (c) 2010-2012 Google, Inc. http://angularjs.org + AngularJS v1.3.0-rc.1 + (c) 2010-2014 Google, Inc. http://angularjs.org License: MIT */ -(function(t,c,B){'use strict';function w(s,r,g,a,h){return{restrict:"ECA",terminal:!0,priority:400,transclude:"element",compile:function(k,d,A){return function(u,k,d){function v(){l&&(l.$destroy(),l=null);m&&(h.leave(m),m=null)}function x(){var f=s.current&&s.current.locals,y=f&&f.$template;if(y){var z=u.$new();A(z,function(e){e.html(y);h.enter(e,null,m||k,function(){!c.isDefined(n)||n&&!u.$eval(n)||r()});v();var p=g(e.contents()),q=s.current;l=q.scope=z;m=e;if(q.controller){f.$scope=l;var d=a(q.controller, -f);q.controllerAs&&(l[q.controllerAs]=d);e.data("$ngControllerController",d);e.children().data("$ngControllerController",d)}p(l);l.$emit("$viewContentLoaded");l.$eval(b)})}else v()}var l,m,n=d.autoscroll,b=d.onload||"";u.$on("$routeChangeSuccess",x);x()}}}}t=c.module("ngRoute",["ng"]).provider("$route",function(){function s(a,h){return c.extend(new (c.extend(function(){},{prototype:a})),h)}function r(a,c){var k=c.caseInsensitiveMatch,d={originalPath:a,regexp:a},g=d.keys=[];a=a.replace(/([().])/g, -"\\$1").replace(/(\/)?:(\w+)([\?|\*])?/g,function(a,c,h,d){a="?"===d?d:null;d="*"===d?d:null;g.push({name:h,optional:!!a});c=c||"";return""+(a?"":c)+"(?:"+(a?c:"")+(d&&"(.+?)"||"([^/]+)")+(a||"")+")"+(a||"")}).replace(/([\/$\*])/g,"\\$1");d.regexp=RegExp("^"+a+"$",k?"i":"");return d}var g={};this.when=function(a,h){g[a]=c.extend({reloadOnSearch:!0},h,a&&r(a,h));if(a){var k="/"==a[a.length-1]?a.substr(0,a.length-1):a+"/";g[k]=c.extend({redirectTo:a},r(k,h))}return this};this.otherwise=function(a){this.when(null, -a);return this};this.$get=["$rootScope","$location","$routeParams","$q","$injector","$http","$templateCache","$sce",function(a,h,k,d,r,u,t,w){function v(){var b=x(),f=n.current;if(b&&f&&b.$$route===f.$$route&&c.equals(b.pathParams,f.pathParams)&&!b.reloadOnSearch&&!m)f.params=b.params,c.copy(f.params,k),a.$broadcast("$routeUpdate",f);else if(b||f)m=!1,a.$broadcast("$routeChangeStart",b,f),(n.current=b)&&b.redirectTo&&(c.isString(b.redirectTo)?h.path(l(b.redirectTo,b.params)).search(b.params).replace(): -h.url(b.redirectTo(b.pathParams,h.path(),h.search())).replace()),d.when(b).then(function(){if(b){var a=c.extend({},b.resolve),f,e;c.forEach(a,function(b,f){a[f]=c.isString(b)?r.get(b):r.invoke(b)});c.isDefined(f=b.template)?c.isFunction(f)&&(f=f(b.params)):c.isDefined(e=b.templateUrl)&&(c.isFunction(e)&&(e=e(b.params)),e=w.getTrustedResourceUrl(e),c.isDefined(e)&&(b.loadedTemplateUrl=e,f=u.get(e,{cache:t}).then(function(b){return b.data})));c.isDefined(f)&&(a.$template=f);return d.all(a)}}).then(function(d){b== -n.current&&(b&&(b.locals=d,c.copy(b.params,k)),a.$broadcast("$routeChangeSuccess",b,f))},function(c){b==n.current&&a.$broadcast("$routeChangeError",b,f,c)})}function x(){var b,a;c.forEach(g,function(d,l){var e;if(e=!a){var p=h.path();e=d.keys;var q={};if(d.regexp)if(p=d.regexp.exec(p)){for(var g=1,k=p.length;g<k;++g){var m=e[g-1],n="string"==typeof p[g]?decodeURIComponent(p[g]):p[g];m&&n&&(q[m.name]=n)}e=q}else e=null;else e=null;e=b=e}e&&(a=s(d,{params:c.extend({},h.search(),b),pathParams:b}),a.$$route= -d)});return a||g[null]&&s(g[null],{params:{},pathParams:{}})}function l(a,d){var g=[];c.forEach((a||"").split(":"),function(a,b){if(0===b)g.push(a);else{var c=a.match(/(\w+)(.*)/),h=c[1];g.push(d[h]);g.push(c[2]||"");delete d[h]}});return g.join("")}var m=!1,n={routes:g,reload:function(){m=!0;a.$evalAsync(v)}};a.$on("$locationChangeSuccess",v);return n}]});t.provider("$routeParams",function(){this.$get=function(){return{}}});t.directive("ngView",w);w.$inject=["$route","$anchorScroll","$compile","$controller", -"$animate"]})(window,window.angular); +(function(r,d,z){'use strict';function v(s,h,f){return{restrict:"ECA",terminal:!0,priority:400,transclude:"element",link:function(a,e,b,g,u){function w(){k&&(k.remove(),k=null);l&&(l.$destroy(),l=null);n&&(f.leave(n).then(function(){k=null}),k=n,n=null)}function t(){var c=s.current&&s.current.locals;if(d.isDefined(c&&c.$template)){var c=a.$new(),m=s.current;n=u(c,function(c){f.enter(c,null,n||e).then(function(){!d.isDefined(p)||p&&!a.$eval(p)||h()});w()});l=m.scope=c;l.$emit("$viewContentLoaded"); +l.$eval(q)}else w()}var l,n,k,p=b.autoscroll,q=b.onload||"";a.$on("$routeChangeSuccess",t);t()}}}function x(d,h,f){return{restrict:"ECA",priority:-400,link:function(a,e){var b=f.current,g=b.locals;e.html(g.$template);var u=d(e.contents());b.controller&&(g.$scope=a,g=h(b.controller,g),b.controllerAs&&(a[b.controllerAs]=g),e.data("$ngControllerController",g),e.children().data("$ngControllerController",g));u(a)}}}r=d.module("ngRoute",["ng"]).provider("$route",function(){function s(a,e){return d.extend(new (d.extend(function(){}, +{prototype:a})),e)}function h(a,d){var b=d.caseInsensitiveMatch,g={originalPath:a,regexp:a},f=g.keys=[];a=a.replace(/([().])/g,"\\$1").replace(/(\/)?:(\w+)([\?\*])?/g,function(a,d,e,b){a="?"===b?b:null;b="*"===b?b:null;f.push({name:e,optional:!!a});d=d||"";return""+(a?"":d)+"(?:"+(a?d:"")+(b&&"(.+?)"||"([^/]+)")+(a||"")+")"+(a||"")}).replace(/([\/$\*])/g,"\\$1");g.regexp=new RegExp("^"+a+"$",b?"i":"");return g}var f={};this.when=function(a,e){f[a]=d.extend({reloadOnSearch:!0},e,a&&h(a,e));if(a){var b= +"/"==a[a.length-1]?a.substr(0,a.length-1):a+"/";f[b]=d.extend({redirectTo:a},h(b,e))}return this};this.otherwise=function(a){"string"===typeof a&&(a={redirectTo:a});this.when(null,a);return this};this.$get=["$rootScope","$location","$routeParams","$q","$injector","$templateRequest","$sce",function(a,e,b,g,h,r,t){function l(){var c=n(),m=q.current;if(c&&m&&c.$$route===m.$$route&&d.equals(c.pathParams,m.pathParams)&&!c.reloadOnSearch&&!p)m.params=c.params,d.copy(m.params,b),a.$broadcast("$routeUpdate", +m);else if(c||m)p=!1,a.$broadcast("$routeChangeStart",c,m),(q.current=c)&&c.redirectTo&&(d.isString(c.redirectTo)?e.path(k(c.redirectTo,c.params)).search(c.params).replace():e.url(c.redirectTo(c.pathParams,e.path(),e.search())).replace()),g.when(c).then(function(){if(c){var a=d.extend({},c.resolve),e,b;d.forEach(a,function(c,b){a[b]=d.isString(c)?h.get(c):h.invoke(c,null,null,b)});d.isDefined(e=c.template)?d.isFunction(e)&&(e=e(c.params)):d.isDefined(b=c.templateUrl)&&(d.isFunction(b)&&(b=b(c.params)), +b=t.getTrustedResourceUrl(b),d.isDefined(b)&&(c.loadedTemplateUrl=b,e=r(b)));d.isDefined(e)&&(a.$template=e);return g.all(a)}}).then(function(e){c==q.current&&(c&&(c.locals=e,d.copy(c.params,b)),a.$broadcast("$routeChangeSuccess",c,m))},function(d){c==q.current&&a.$broadcast("$routeChangeError",c,m,d)})}function n(){var c,a;d.forEach(f,function(b,g){var f;if(f=!a){var h=e.path();f=b.keys;var l={};if(b.regexp)if(h=b.regexp.exec(h)){for(var k=1,n=h.length;k<n;++k){var p=f[k-1],q=h[k];p&&q&&(l[p.name]= +q)}f=l}else f=null;else f=null;f=c=f}f&&(a=s(b,{params:d.extend({},e.search(),c),pathParams:c}),a.$$route=b)});return a||f[null]&&s(f[null],{params:{},pathParams:{}})}function k(a,b){var e=[];d.forEach((a||"").split(":"),function(a,c){if(0===c)e.push(a);else{var d=a.match(/(\w+)(.*)/),f=d[1];e.push(b[f]);e.push(d[2]||"");delete b[f]}});return e.join("")}var p=!1,q={routes:f,reload:function(){p=!0;a.$evalAsync(l)},updateParams:function(a){if(this.current&&this.current.$$route){var b={},f=this;d.forEach(Object.keys(a), +function(d){f.current.pathParams[d]||(b[d]=a[d])});a=d.extend({},this.current.params,a);e.path(k(this.current.$$route.originalPath,a));e.search(d.extend({},e.search(),b))}else throw y("norout");}};a.$on("$locationChangeSuccess",l);return q}]});var y=d.$$minErr("ngRoute");r.provider("$routeParams",function(){this.$get=function(){return{}}});r.directive("ngView",v);r.directive("ngView",x);v.$inject=["$route","$anchorScroll","$animate"];x.$inject=["$compile","$controller","$route"]})(window,window.angular); //# sourceMappingURL=angular-route.min.js.map diff --git a/webclient/js/angular-sanitize.js b/webclient/js/angular-sanitize.js index d34522ac8d..ec46895f68 100644 --- a/webclient/js/angular-sanitize.js +++ b/webclient/js/angular-sanitize.js @@ -1,6 +1,6 @@ /** - * @license AngularJS v1.2.0 - * (c) 2010-2012 Google, Inc. http://angularjs.org + * @license AngularJS v1.3.0-rc.1 + * (c) 2010-2014 Google, Inc. http://angularjs.org * License: MIT */ (function(window, angular, undefined) {'use strict'; @@ -8,7 +8,7 @@ var $sanitizeMinErr = angular.$$minErr('$sanitize'); /** - * @ngdoc overview + * @ngdoc module * @name ngSanitize * @description * @@ -16,7 +16,6 @@ var $sanitizeMinErr = angular.$$minErr('$sanitize'); * * The `ngSanitize` module provides functionality to sanitize HTML. * - * {@installModule sanitize} * * <div doc-module-components="ngSanitize"></div> * @@ -42,8 +41,8 @@ var $sanitizeMinErr = angular.$$minErr('$sanitize'); /** * @ngdoc service - * @name ngSanitize.$sanitize - * @function + * @name $sanitize + * @kind function * * @description * The input is sanitized by parsing the html into tokens. All safe tokens (from a whitelist) are @@ -51,25 +50,28 @@ var $sanitizeMinErr = angular.$$minErr('$sanitize'); * it into the returned string, however, since our parser is more strict than a typical browser * parser, it's possible that some obscure input, which would be recognized as valid HTML by a * browser, won't make it through the sanitizer. + * The whitelist is configured using the functions `aHrefSanitizationWhitelist` and + * `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider `$compileProvider`}. * * @param {string} html Html input. * @returns {string} Sanitized html. * * @example - <doc:example module="ngSanitize"> - <doc:source> + <example module="sanitizeExample" deps="angular-sanitize.js"> + <file name="index.html"> <script> - function Ctrl($scope, $sce) { - $scope.snippet = - '<p style="color:blue">an html\n' + - '<em onmouseover="this.textContent=\'PWN3D!\'">click here</em>\n' + - 'snippet</p>'; - $scope.deliberatelyTrustDangerousSnippet = function() { - return $sce.trustAsHtml($scope.snippet); - }; - } + angular.module('sanitizeExample', ['ngSanitize']) + .controller('ExampleController', ['$scope', '$sce', function($scope, $sce) { + $scope.snippet = + '<p style="color:blue">an html\n' + + '<em onmouseover="this.textContent=\'PWN3D!\'">click here</em>\n' + + 'snippet</p>'; + $scope.deliberatelyTrustDangerousSnippet = function() { + return $sce.trustAsHtml($scope.snippet); + }; + }]); </script> - <div ng-controller="Ctrl"> + <div ng-controller="ExampleController"> Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea> <table> <tr> @@ -101,56 +103,71 @@ var $sanitizeMinErr = angular.$$minErr('$sanitize'); </tr> </table> </div> - </doc:source> - <doc:scenario> + </file> + <file name="protractor.js" type="protractor"> it('should sanitize the html snippet by default', function() { - expect(using('#bind-html-with-sanitize').element('div').html()). + expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()). toBe('<p>an html\n<em>click here</em>\nsnippet</p>'); }); it('should inline raw snippet if bound to a trusted value', function() { - expect(using('#bind-html-with-trust').element("div").html()). + expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()). 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(using('#bind-default').element('div').html()). + expect(element(by.css('#bind-default div')).getInnerHtml()). toBe("<p style=\"color:blue\">an html\n" + "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" + "snippet</p>"); }); it('should update', function() { - input('snippet').enter('new <b onclick="alert(1)">text</b>'); - expect(using('#bind-html-with-sanitize').element('div').html()).toBe('new <b>text</b>'); - expect(using('#bind-html-with-trust').element('div').html()).toBe( + 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()). + toBe('new <b>text</b>'); + expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()).toBe( 'new <b onclick="alert(1)">text</b>'); - expect(using('#bind-default').element('div').html()).toBe( + expect(element(by.css('#bind-default div')).getInnerHtml()).toBe( "new <b onclick=\"alert(1)\">text</b>"); }); - </doc:scenario> - </doc:example> + </file> + </example> */ -var $sanitize = function(html) { +function $SanitizeProvider() { + this.$get = ['$$sanitizeUri', function($$sanitizeUri) { + return function(html) { + var buf = []; + htmlParser(html, htmlSanitizeWriter(buf, function(uri, isImage) { + return !/^unsafe/.test($$sanitizeUri(uri, isImage)); + })); + return buf.join(''); + }; + }]; +} + +function sanitizeText(chars) { var buf = []; - htmlParser(html, htmlSanitizeWriter(buf)); - return buf.join(''); -}; + var writer = htmlSanitizeWriter(buf, angular.noop); + writer.chars(chars); + return buf.join(''); +} // Regular Expressions for parsing tags and attributes var START_TAG_REGEXP = - /^<\s*([\w:-]+)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*>/, - END_TAG_REGEXP = /^<\s*\/\s*([\w:-]+)[^>]*>/, + /^<((?:[a-zA-Z])[\w:-]*)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*(>?)/, + END_TAG_REGEXP = /^<\/\s*([\w:-]+)[^>]*>/, ATTR_REGEXP = /([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g, BEGIN_TAG_REGEXP = /^</, - BEGING_END_TAGE_REGEXP = /^<\s*\//, + BEGING_END_TAGE_REGEXP = /^<\//, COMMENT_REGEXP = /<!--(.*?)-->/g, DOCTYPE_REGEXP = /<!DOCTYPE([^>]*?)>/i, CDATA_REGEXP = /<!\[CDATA\[(.*?)]]>/g, - URI_REGEXP = /^((ftp|https?):\/\/|mailto:|tel:|#)/i, + SURROGATE_PAIR_REGEXP = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g, // Match everything outside of normal chars and " (quote character) NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g; @@ -197,7 +214,7 @@ var validAttrs = angular.extend({}, uriAttrs, makeMap( 'abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,'+ 'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,'+ 'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,'+ - 'scope,scrolling,shape,span,start,summary,target,title,type,'+ + 'scope,scrolling,shape,size,span,start,summary,target,title,type,'+ 'valign,value,vspace,width')); function makeMap(str) { @@ -220,10 +237,18 @@ function makeMap(str) { * @param {object} handler */ function htmlParser( html, handler ) { - var index, chars, match, stack = [], last = html; + if (typeof html !== 'string') { + if (html === null || typeof html === 'undefined') { + html = ''; + } else { + html = '' + html; + } + } + var index, chars, match, stack = [], last = html, text; stack.last = function() { return stack[ stack.length - 1 ]; }; while ( html ) { + text = ''; chars = true; // Make sure we're not in a script or style element @@ -244,7 +269,7 @@ function htmlParser( html, handler ) { match = html.match( DOCTYPE_REGEXP ); if ( match ) { - html = html.replace( match[0] , ''); + html = html.replace( match[0], ''); chars = false; } // end tag @@ -262,16 +287,23 @@ function htmlParser( html, handler ) { match = html.match( START_TAG_REGEXP ); if ( match ) { - html = html.substring( match[0].length ); - match[0].replace( START_TAG_REGEXP, parseStartTag ); + // We only have a valid start-tag if there is a '>'. + if ( match[4] ) { + html = html.substring( match[0].length ); + match[0].replace( START_TAG_REGEXP, parseStartTag ); + } chars = false; + } else { + // no ending tag found --- this piece should be encoded as an entity. + text += '<'; + html = html.substring(1); } } if ( chars ) { index = html.indexOf("<"); - var text = index < 0 ? html : html.substring( 0, index ); + text += index < 0 ? html : html.substring( 0, index ); html = index < 0 ? "" : html.substring( index ); if (handler.chars) handler.chars( decodeEntities(text) ); @@ -351,15 +383,32 @@ function htmlParser( html, handler ) { } } +var hiddenPre=document.createElement("pre"); +var spaceRe = /^(\s*)([\s\S]*?)(\s*)$/; /** * decodes all entities into regular string * @param value * @returns {string} A string with decoded entities. */ -var hiddenPre=document.createElement("pre"); function decodeEntities(value) { - hiddenPre.innerHTML=value.replace(/</g,"<"); - return hiddenPre.innerText || hiddenPre.textContent || ''; + if (!value) { return ''; } + + // Note: IE8 does not preserve spaces at the start/end of innerHTML + // so we must capture them and reattach them afterward + var parts = spaceRe.exec(value); + var spaceBefore = parts[1]; + var spaceAfter = parts[3]; + var content = parts[2]; + if (content) { + hiddenPre.innerHTML=content.replace(/</g,"<"); + // innerText depends on styling as it doesn't display hidden elements. + // Therefore, it's better to use textContent not to cause unnecessary + // reflows. However, IE<9 don't support textContent so the innerText + // fallback is necessary. + content = 'textContent' in hiddenPre ? + hiddenPre.textContent : hiddenPre.innerText; + } + return spaceBefore + content + spaceAfter; } /** @@ -367,11 +416,16 @@ function decodeEntities(value) { * resulting string can be safely inserted into attribute or * element text. * @param value - * @returns escaped text + * @returns {string} escaped text */ function encodeEntities(value) { return value. replace(/&/g, '&'). + replace(SURROGATE_PAIR_REGEXP, function (value) { + var hi = value.charCodeAt(0); + var low = value.charCodeAt(1); + return '&#' + (((hi - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000) + ';'; + }). replace(NON_ALPHANUMERIC_REGEXP, function(value){ return '&#' + value.charCodeAt(0) + ';'; }). @@ -389,7 +443,7 @@ function encodeEntities(value) { * comment: function(text) {} * } */ -function htmlSanitizeWriter(buf){ +function htmlSanitizeWriter(buf, uriValidator){ var ignore = false; var out = angular.bind(buf, buf.push); return { @@ -403,7 +457,9 @@ function htmlSanitizeWriter(buf){ out(tag); angular.forEach(attrs, function(value, key){ var lkey=angular.lowercase(key); - if (validAttrs[lkey]===true && (uriAttrs[lkey]!==true || value.match(URI_REGEXP))) { + var isImage = (tag === 'img' && lkey === 'src') || (lkey === 'background'); + if (validAttrs[lkey] === true && + (uriAttrs[lkey] !== true || uriValidator(value, isImage))) { out(' '); out(key); out('="'); @@ -435,14 +491,14 @@ function htmlSanitizeWriter(buf){ // define ngSanitize module and register $sanitize service -angular.module('ngSanitize', []).value('$sanitize', $sanitize); +angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider); -/* global htmlSanitizeWriter: false */ +/* global sanitizeText: false */ /** * @ngdoc filter - * @name ngSanitize.filter:linky - * @function + * @name linky + * @kind function * * @description * Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and @@ -458,20 +514,21 @@ angular.module('ngSanitize', []).value('$sanitize', $sanitize); <span ng-bind-html="linky_expression | linky"></span> * * @example - <doc:example module="ngSanitize"> - <doc:source> + <example module="linkyExample" deps="angular-sanitize.js"> + <file name="index.html"> <script> - function Ctrl($scope) { - $scope.snippet = - 'Pretty text with some links:\n'+ - 'http://angularjs.org/,\n'+ - 'mailto:us@somewhere.org,\n'+ - 'another@somewhere.org,\n'+ - 'and one more: ftp://127.0.0.1/.'; - $scope.snippetWithTarget = 'http://angularjs.org/'; - } + angular.module('linkyExample', ['ngSanitize']) + .controller('ExampleController', ['$scope', function($scope) { + $scope.snippet = + 'Pretty text with some links:\n'+ + 'http://angularjs.org/,\n'+ + 'mailto:us@somewhere.org,\n'+ + 'another@somewhere.org,\n'+ + 'and one more: ftp://127.0.0.1/.'; + $scope.snippetWithTarget = 'http://angularjs.org/'; + }]); </script> - <div ng-controller="Ctrl"> + <div ng-controller="ExampleController"> Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea> <table> <tr> @@ -503,43 +560,44 @@ angular.module('ngSanitize', []).value('$sanitize', $sanitize); <td><div ng-bind="snippet"></div></td> </tr> </table> - </doc:source> - <doc:scenario> + </file> + <file name="protractor.js" type="protractor"> it('should linkify the snippet with urls', function() { - expect(using('#linky-filter').binding('snippet | linky')). - toBe('Pretty text with some links: ' + - '<a href="http://angularjs.org/">http://angularjs.org/</a>, ' + - '<a href="mailto:us@somewhere.org">us@somewhere.org</a>, ' + - '<a href="mailto:another@somewhere.org">another@somewhere.org</a>, ' + - 'and one more: <a href="ftp://127.0.0.1/">ftp://127.0.0.1/</a>.'); + expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()). + toBe('Pretty text with some links: http://angularjs.org/, us@somewhere.org, ' + + 'another@somewhere.org, and one more: ftp://127.0.0.1/.'); + expect(element.all(by.css('#linky-filter a')).count()).toEqual(4); }); - it ('should not linkify snippet without the linky filter', function() { - expect(using('#escaped-html').binding('snippet')). - toBe("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/."); + it('should not linkify snippet without the linky filter', function() { + expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()). + toBe('Pretty text with some links: http://angularjs.org/, mailto:us@somewhere.org, ' + + 'another@somewhere.org, and one more: ftp://127.0.0.1/.'); + expect(element.all(by.css('#escaped-html a')).count()).toEqual(0); }); it('should update', function() { - input('snippet').enter('new http://link.'); - expect(using('#linky-filter').binding('snippet | linky')). - toBe('new <a href="http://link">http://link</a>.'); - expect(using('#escaped-html').binding('snippet')).toBe('new http://link.'); + element(by.model('snippet')).clear(); + element(by.model('snippet')).sendKeys('new http://link.'); + expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()). + toBe('new http://link.'); + expect(element.all(by.css('#linky-filter a')).count()).toEqual(1); + expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()) + .toBe('new http://link.'); }); it('should work with the target property', function() { - expect(using('#linky-target').binding("snippetWithTarget | linky:'_blank'")). - toBe('<a target="_blank" href="http://angularjs.org/">http://angularjs.org/</a>'); + expect(element(by.id('linky-target')). + element(by.binding("snippetWithTarget | linky:'_blank'")).getText()). + toBe('http://angularjs.org/'); + expect(element(by.css('#linky-target a')).getAttribute('target')).toEqual('_blank'); }); - </doc:scenario> - </doc:example> + </file> + </example> */ -angular.module('ngSanitize').filter('linky', function() { +angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) { var LINKY_URL_REGEXP = - /((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>]/, + /((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"]/, MAILTO_REGEXP = /^mailto:/; return function(text, target) { @@ -547,31 +605,43 @@ angular.module('ngSanitize').filter('linky', function() { var match; var raw = text; var html = []; - // TODO(vojta): use $sanitize instead - var writer = htmlSanitizeWriter(html); var url; var i; - var properties = {}; - if (angular.isDefined(target)) { - properties.target = target; - } while ((match = raw.match(LINKY_URL_REGEXP))) { // We can not end in these as they are sometimes found at the end of the sentence url = match[0]; // if we did not match ftp/http/mailto then assume mailto if (match[2] == match[3]) url = 'mailto:' + url; i = match.index; - writer.chars(raw.substr(0, i)); - properties.href = url; - writer.start('a', properties); - writer.chars(match[0].replace(MAILTO_REGEXP, '')); - writer.end('a'); + addText(raw.substr(0, i)); + addLink(url, match[0].replace(MAILTO_REGEXP, '')); raw = raw.substring(i + match[0].length); } - writer.chars(raw); - return html.join(''); + addText(raw); + return $sanitize(html.join('')); + + function addText(text) { + if (!text) { + return; + } + html.push(sanitizeText(text)); + } + + function addLink(url, text) { + html.push('<a '); + if (angular.isDefined(target)) { + html.push('target="'); + html.push(target); + html.push('" '); + } + html.push('href="'); + html.push(url); + html.push('">'); + addText(text); + html.push('</a>'); + } }; -}); +}]); })(window, window.angular); diff --git a/webclient/js/angular-sanitize.min.js b/webclient/js/angular-sanitize.min.js index 15bfb59ab7..ce99bba18e 100644 --- a/webclient/js/angular-sanitize.min.js +++ b/webclient/js/angular-sanitize.min.js @@ -1,14 +1,15 @@ /* - AngularJS v1.2.0 - (c) 2010-2012 Google, Inc. http://angularjs.org + AngularJS v1.3.0-rc.1 + (c) 2010-2014 Google, Inc. http://angularjs.org License: MIT */ -(function(m,g,n){'use strict';function h(a){var d={};a=a.split(",");var c;for(c=0;c<a.length;c++)d[a[c]]=!0;return d}function D(a,d){function c(a,b,c,f){b=g.lowercase(b);if(r[b])for(;e.last()&&s[e.last()];)k("",e.last());t[b]&&e.last()==b&&k("",b);(f=u[b]||!!f)||e.push(b);var l={};c.replace(E,function(a,b,d,c,e){l[b]=p(d||c||e||"")});d.start&&d.start(b,l,f)}function k(a,b){var c=0,k;if(b=g.lowercase(b))for(c=e.length-1;0<=c&&e[c]!=b;c--);if(0<=c){for(k=e.length-1;k>=c;k--)d.end&&d.end(e[k]);e.length= -c}}var b,f,e=[],l=a;for(e.last=function(){return e[e.length-1]};a;){f=!0;if(e.last()&&v[e.last()])a=a.replace(RegExp("(.*)<\\s*\\/\\s*"+e.last()+"[^>]*>","i"),function(a,b){b=b.replace(F,"$1").replace(G,"$1");d.chars&&d.chars(p(b));return""}),k("",e.last());else{if(0===a.indexOf("\x3c!--"))b=a.indexOf("--",4),0<=b&&a.lastIndexOf("--\x3e",b)===b&&(d.comment&&d.comment(a.substring(4,b)),a=a.substring(b+3),f=!1);else if(w.test(a)){if(b=a.match(w))a=a.replace(b[0],""),f=!1}else if(H.test(a)){if(b=a.match(x))a= -a.substring(b[0].length),b[0].replace(x,k),f=!1}else I.test(a)&&(b=a.match(y))&&(a=a.substring(b[0].length),b[0].replace(y,c),f=!1);f&&(b=a.indexOf("<"),f=0>b?a:a.substring(0,b),a=0>b?"":a.substring(b),d.chars&&d.chars(p(f)))}if(a==l)throw J("badparse",a);l=a}k()}function p(a){q.innerHTML=a.replace(/</g,"<");return q.innerText||q.textContent||""}function z(a){return a.replace(/&/g,"&").replace(K,function(a){return"&#"+a.charCodeAt(0)+";"}).replace(/</g,"<").replace(/>/g,">")}function A(a){var d= -!1,c=g.bind(a,a.push);return{start:function(a,b,f){a=g.lowercase(a);!d&&v[a]&&(d=a);d||!0!==B[a]||(c("<"),c(a),g.forEach(b,function(a,b){var d=g.lowercase(b);!0!==L[d]||!0===C[d]&&!a.match(M)||(c(" "),c(b),c('="'),c(z(a)),c('"'))}),c(f?"/>":">"))},end:function(a){a=g.lowercase(a);d||!0!==B[a]||(c("</"),c(a),c(">"));a==d&&(d=!1)},chars:function(a){d||c(z(a))}}}var J=g.$$minErr("$sanitize"),y=/^<\s*([\w:-]+)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*>/,x=/^<\s*\/\s*([\w:-]+)[^>]*>/, -E=/([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g,I=/^</,H=/^<\s*\//,F=/\x3c!--(.*?)--\x3e/g,w=/<!DOCTYPE([^>]*?)>/i,G=/<!\[CDATA\[(.*?)]]\x3e/g,M=/^((ftp|https?):\/\/|mailto:|tel:|#)/i,K=/([^\#-~| |!])/g,u=h("area,br,col,hr,img,wbr");m=h("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr");n=h("rp,rt");var t=g.extend({},n,m),r=g.extend({},m,h("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,script,section,table,ul")), -s=g.extend({},n,h("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")),v=h("script,style"),B=g.extend({},u,r,s,t),C=h("background,cite,href,longdesc,src,usemap"),L=g.extend({},C,h("abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,scope,scrolling,shape,span,start,summary,target,title,type,valign,value,vspace,width")), -q=document.createElement("pre");g.module("ngSanitize",[]).value("$sanitize",function(a){var d=[];D(a,A(d));return d.join("")});g.module("ngSanitize").filter("linky",function(){var a=/((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>]/,d=/^mailto:/;return function(c,k){if(!c)return c;var b,f=c,e=[],l=A(e),h,m,n={};g.isDefined(k)&&(n.target=k);for(;b=f.match(a);)h=b[0],b[2]==b[3]&&(h="mailto:"+h),m=b.index,l.chars(f.substr(0,m)),n.href=h,l.start("a",n),l.chars(b[0].replace(d,"")),l.end("a"), -f=f.substring(m+b[0].length);l.chars(f);return e.join("")}})})(window,window.angular); +(function(q,g,r){'use strict';function F(a){var d=[];t(d,g.noop).chars(a);return d.join("")}function m(a){var d={};a=a.split(",");var c;for(c=0;c<a.length;c++)d[a[c]]=!0;return d}function G(a,d){function c(a,b,c,h){b=g.lowercase(b);if(u[b])for(;f.last()&&v[f.last()];)e("",f.last());w[b]&&f.last()==b&&e("",b);(h=x[b]||!!h)||f.push(b);var n={};c.replace(H,function(a,b,d,c,e){n[b]=s(d||c||e||"")});d.start&&d.start(b,n,h)}function e(a,b){var c=0,e;if(b=g.lowercase(b))for(c=f.length-1;0<=c&&f[c]!=b;c--); +if(0<=c){for(e=f.length-1;e>=c;e--)d.end&&d.end(f[e]);f.length=c}}"string"!==typeof a&&(a=null===a||"undefined"===typeof a?"":""+a);var b,l,f=[],n=a,h;for(f.last=function(){return f[f.length-1]};a;){h="";l=!0;if(f.last()&&y[f.last()])a=a.replace(new RegExp("(.*)<\\s*\\/\\s*"+f.last()+"[^>]*>","i"),function(a,b){b=b.replace(I,"$1").replace(J,"$1");d.chars&&d.chars(s(b));return""}),e("",f.last());else{if(0===a.indexOf("\x3c!--"))b=a.indexOf("--",4),0<=b&&a.lastIndexOf("--\x3e",b)===b&&(d.comment&&d.comment(a.substring(4, +b)),a=a.substring(b+3),l=!1);else if(z.test(a)){if(b=a.match(z))a=a.replace(b[0],""),l=!1}else if(K.test(a)){if(b=a.match(A))a=a.substring(b[0].length),b[0].replace(A,e),l=!1}else L.test(a)&&((b=a.match(B))?(b[4]&&(a=a.substring(b[0].length),b[0].replace(B,c)),l=!1):(h+="<",a=a.substring(1)));l&&(b=a.indexOf("<"),h+=0>b?a:a.substring(0,b),a=0>b?"":a.substring(b),d.chars&&d.chars(s(h)))}if(a==n)throw M("badparse",a);n=a}e()}function s(a){if(!a)return"";var d=N.exec(a);a=d[1];var c=d[3];if(d=d[2])p.innerHTML= +d.replace(/</g,"<"),d="textContent"in p?p.textContent:p.innerText;return a+d+c}function C(a){return a.replace(/&/g,"&").replace(O,function(a){var c=a.charCodeAt(0);a=a.charCodeAt(1);return"&#"+(1024*(c-55296)+(a-56320)+65536)+";"}).replace(P,function(a){return"&#"+a.charCodeAt(0)+";"}).replace(/</g,"<").replace(/>/g,">")}function t(a,d){var c=!1,e=g.bind(a,a.push);return{start:function(a,l,f){a=g.lowercase(a);!c&&y[a]&&(c=a);c||!0!==D[a]||(e("<"),e(a),g.forEach(l,function(c,f){var k= +g.lowercase(f),l="img"===a&&"src"===k||"background"===k;!0!==Q[k]||!0===E[k]&&!d(c,l)||(e(" "),e(f),e('="'),e(C(c)),e('"'))}),e(f?"/>":">"))},end:function(a){a=g.lowercase(a);c||!0!==D[a]||(e("</"),e(a),e(">"));a==c&&(c=!1)},chars:function(a){c||e(C(a))}}}var M=g.$$minErr("$sanitize"),B=/^<((?:[a-zA-Z])[\w:-]*)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*(>?)/,A=/^<\/\s*([\w:-]+)[^>]*>/,H=/([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g,L=/^</, +K=/^<\//,I=/\x3c!--(.*?)--\x3e/g,z=/<!DOCTYPE([^>]*?)>/i,J=/<!\[CDATA\[(.*?)]]\x3e/g,O=/[\uD800-\uDBFF][\uDC00-\uDFFF]/g,P=/([^\#-~| |!])/g,x=m("area,br,col,hr,img,wbr");q=m("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr");r=m("rp,rt");var w=g.extend({},r,q),u=g.extend({},q,m("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,script,section,table,ul")),v=g.extend({},r,m("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")), +y=m("script,style"),D=g.extend({},x,u,v,w),E=m("background,cite,href,longdesc,src,usemap"),Q=g.extend({},E,m("abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,scope,scrolling,shape,size,span,start,summary,target,title,type,valign,value,vspace,width")),p=document.createElement("pre"),N=/^(\s*)([\s\S]*?)(\s*)$/;g.module("ngSanitize",[]).provider("$sanitize", +function(){this.$get=["$$sanitizeUri",function(a){return function(d){var c=[];G(d,t(c,function(c,b){return!/^unsafe/.test(a(c,b))}));return c.join("")}}]});g.module("ngSanitize").filter("linky",["$sanitize",function(a){var d=/((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"]/,c=/^mailto:/;return function(e,b){function l(a){a&&k.push(F(a))}function f(a,c){k.push("<a ");g.isDefined(b)&&(k.push('target="'),k.push(b),k.push('" '));k.push('href="');k.push(a);k.push('">');l(c);k.push("</a>")} +if(!e)return e;for(var n,h=e,k=[],m,p;n=h.match(d);)m=n[0],n[2]==n[3]&&(m="mailto:"+m),p=n.index,l(h.substr(0,p)),f(m,n[0].replace(c,"")),h=h.substring(p+n[0].length);l(h);return a(k.join(""))}}])})(window,window.angular); //# sourceMappingURL=angular-sanitize.min.js.map diff --git a/webclient/js/angular.js b/webclient/js/angular.js index 217c4036fc..bdc97abb02 100644 --- a/webclient/js/angular.js +++ b/webclient/js/angular.js @@ -1,5 +1,5 @@ /** - * @license AngularJS v1.2.20 + * @license AngularJS v1.3.0-rc.1 * (c) 2010-2014 Google, Inc. http://angularjs.org * License: MIT */ @@ -30,10 +30,13 @@ * should all be static strings, not variables or general expressions. * * @param {string} module The namespace to use for the new minErr instance. + * @param {function} ErrorConstructor Custom error constructor to be instantiated when returning + * error from returned function, for cases when a particular type of error is useful. * @returns {function(code:string, template:string, ...templateArgs): Error} minErr instance */ -function minErr(module) { +function minErr(module, ErrorConstructor) { + ErrorConstructor = ErrorConstructor || Error; return function () { var code = arguments[0], prefix = '[' + (module ? module + ':' : '') + code + '] ', @@ -68,101 +71,99 @@ function minErr(module) { return match; }); - message = message + '\nhttp://errors.angularjs.org/1.2.20/' + + message = message + '\nhttp://errors.angularjs.org/1.3.0-rc.1/' + (module ? module + '/' : '') + code; for (i = 2; i < arguments.length; i++) { message = message + (i == 2 ? '?' : '&') + 'p' + (i-2) + '=' + encodeURIComponent(stringify(arguments[i])); } - - return new Error(message); + return new ErrorConstructor(message); }; } /* We need to tell jshint what variables are being exported */ -/* global - -angular, - -msie, - -jqLite, - -jQuery, - -slice, - -push, - -toString, - -ngMinErr, - -angularModule, - -nodeName_, - -uid, - -VALIDITY_STATE_PROPERTY, - - -lowercase, - -uppercase, - -manualLowercase, - -manualUppercase, - -nodeName_, - -isArrayLike, - -forEach, - -sortedKeys, - -forEachSorted, - -reverseParams, - -nextUid, - -setHashKey, - -extend, - -int, - -inherit, - -noop, - -identity, - -valueFn, - -isUndefined, - -isDefined, - -isObject, - -isString, - -isNumber, - -isDate, - -isArray, - -isFunction, - -isRegExp, - -isWindow, - -isScope, - -isFile, - -isBlob, - -isBoolean, - -trim, - -isElement, - -makeMap, - -map, - -size, - -includes, - -indexOf, - -arrayRemove, - -isLeafNode, - -copy, - -shallowCopy, - -equals, - -csp, - -concat, - -sliceArgs, - -bind, - -toJsonReplacer, - -toJson, - -fromJson, - -toBoolean, - -startingTag, - -tryDecodeURIComponent, - -parseKeyValue, - -toKeyValue, - -encodeUriSegment, - -encodeUriQuery, - -angularInit, - -bootstrap, - -snake_case, - -bindJQuery, - -assertArg, - -assertArgFn, - -assertNotHasOwnProperty, - -getter, - -getBlockElements, - -hasOwnProperty, - +/* global angular: true, + msie: true, + jqLite: true, + jQuery: true, + slice: 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, + sortedKeys: true, + forEachSorted: true, + reverseParams: true, + nextUid: true, + setHashKey: true, + extend: true, + int: true, + inherit: true, + noop: true, + identity: true, + valueFn: true, + isUndefined: true, + isDefined: true, + isObject: true, + isString: true, + isNumber: true, + isDate: true, + isArray: true, + isFunction: true, + isRegExp: true, + isWindow: true, + isScope: true, + isFile: true, + isBlob: true, + isBoolean: true, + isPromiseLike: true, + trim: true, + isElement: true, + makeMap: true, + map: true, + size: true, + includes: true, + arrayRemove: true, + isLeafNode: true, + copy: true, + shallowCopy: true, + equals: true, + csp: true, + concat: true, + sliceArgs: true, + bind: true, + toJsonReplacer: true, + toJson: true, + fromJson: 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, */ //////////////////////////////////// @@ -182,6 +183,8 @@ function minErr(module) { * <div doc-module-components="ng"></div> */ +var REGEX_STRING_REGEXP = /^\/(.+)\/([a-z]*)$/; + // The name of a form control's ValidityState property. // This is used so that it's possible for internal tests to create mock ValidityStates. var VALIDITY_STATE_PROPERTY = 'validity'; @@ -247,8 +250,7 @@ var /** holds major version number for IE or NaN for real browsers */ /** @name angular */ angular = window.angular || (window.angular = {}), angularModule, - nodeName_, - uid = ['0', '0', '0']; + uid = 0; /** * IE 11 changed the format of the UserAgent string. @@ -289,9 +291,9 @@ function isArrayLike(obj) { * * @description * Invokes the `iterator` function once for each item in `obj` collection, which can be either an - * object or an array. The `iterator` function is invoked with `iterator(value, key)`, where `value` - * is the value of an object property or an array element and `key` is the object property key or - * array element index. Specifying a `context` for the function is optional. + * object or an array. The `iterator` function is invoked with `iterator(value, key, obj)`, where `value` + * is the value of an object property or an array element, `key` is the object property key or + * array element index and obj is the `obj` itself. Specifying a `context` for the function is optional. * * It is worth noting that `.forEach` does not iterate over inherited properties because it filters * using the `hasOwnProperty` method. @@ -310,26 +312,31 @@ function isArrayLike(obj) { * @param {Object=} context Object to become context (`this`) for the iterator function. * @returns {Object|Array} Reference to `obj`. */ + function forEach(obj, iterator, context) { - var key; + var key, length; if (obj) { if (isFunction(obj)) { for (key in obj) { // Need to check if hasOwnProperty exists, // as on IE8 the result of querySelectorAll is an object without a hasOwnProperty function if (key != 'prototype' && key != 'length' && key != 'name' && (!obj.hasOwnProperty || obj.hasOwnProperty(key))) { - iterator.call(context, obj[key], key); + iterator.call(context, obj[key], key, obj); + } + } + } else if (isArray(obj) || isArrayLike(obj)) { + var isPrimitive = typeof obj !== 'object'; + for (key = 0, length = obj.length; key < length; key++) { + if (isPrimitive || key in obj) { + iterator.call(context, obj[key], key, obj); } } } else if (obj.forEach && obj.forEach !== forEach) { - obj.forEach(iterator, context); - } else if (isArrayLike(obj)) { - for (key = 0; key < obj.length; key++) - iterator.call(context, obj[key], key); + obj.forEach(iterator, context, obj); } else { for (key in obj) { if (obj.hasOwnProperty(key)) { - iterator.call(context, obj[key], key); + iterator.call(context, obj[key], key, obj); } } } @@ -366,33 +373,17 @@ function reverseParams(iteratorFn) { } /** - * A consistent way of creating unique IDs in angular. The ID is a sequence of alpha numeric - * characters such as '012ABC'. The reason why we are not using simply a number counter is that - * the number string gets longer over time, and it can also overflow, where as the nextId - * will grow much slower, it is a string, and it will never overflow. + * A consistent way of creating unique IDs in angular. + * + * Using simple numbers allows us to generate 28.6 million unique ids per second for 10 years before + * we hit number precision issues in JavaScript. + * + * Math.pow(2,53) / 60 / 60 / 24 / 365 / 10 = 28.6M * - * @returns {string} an unique alpha-numeric string + * @returns {number} an unique alpha-numeric string */ function nextUid() { - var index = uid.length; - var digit; - - while(index) { - index--; - digit = uid[index].charCodeAt(0); - if (digit == 57 /*'9'*/) { - uid[index] = 'A'; - return uid.join(''); - } - if (digit == 90 /*'Z'*/) { - uid[index] = '0'; - } else { - uid[index] = String.fromCharCode(digit + 1); - return uid.join(''); - } - } - uid.unshift('0'); - return uid.join(''); + return ++uid; } @@ -426,15 +417,19 @@ function setHashKey(obj, h) { */ function extend(dst) { var h = dst.$$hashKey; - forEach(arguments, function(obj) { - if (obj !== dst) { - forEach(obj, function(value, key) { - dst[key] = value; - }); + + for (var i = 1, ii = arguments.length; i < ii; i++) { + var obj = arguments[i]; + if (obj) { + var keys = Object.keys(obj); + for (var j = 0, jj = keys.length; j < jj; j++) { + var key = keys[j]; + dst[key] = obj[key]; + } } - }); + } - setHashKey(dst,h); + setHashKey(dst, h); return dst; } @@ -532,7 +527,10 @@ function isDefined(value){return typeof value !== 'undefined';} * @param {*} value Reference to check. * @returns {boolean} True if `value` is an `Object` but not `null`. */ -function isObject(value){return value != null && typeof value === 'object';} +function isObject(value){ + // http://jsperf.com/isobject4 + return value !== null && typeof value === 'object'; +} /** @@ -594,14 +592,7 @@ function isDate(value) { * @param {*} value Reference to check. * @returns {boolean} True if `value` is an `Array`. */ -var isArray = (function() { - if (!isFunction(Array.isArray)) { - return function(value) { - return toString.call(value) === '[object Array]'; - }; - } - return Array.isArray; -})(); +var isArray = Array.isArray; /** * @ngdoc function @@ -638,7 +629,7 @@ function isRegExp(value) { * @returns {boolean} True if `obj` is a window obj. */ function isWindow(obj) { - return obj && obj.document && obj.location && obj.alert && obj.setInterval; + return obj && obj.window === obj; } @@ -662,19 +653,14 @@ function isBoolean(value) { } -var trim = (function() { - // native trim is way faster: http://jsperf.com/angular-trim-test - // but IE doesn't have it... :-( - // TODO: we should move this into IE/ES5 polyfill - if (!String.prototype.trim) { - return function(value) { - return isString(value) ? value.replace(/^\s\s*/, '').replace(/\s\s*$/, '') : value; - }; - } - return function(value) { - return isString(value) ? value.trim() : value; - }; -})(); +function isPromiseLike(obj) { + return obj && isFunction(obj.then); +} + + +var trim = function(value) { + return isString(value) ? value.trim() : value; +}; /** @@ -707,16 +693,8 @@ function makeMap(str) { } -if (msie < 9) { - nodeName_ = function(element) { - element = element.nodeName ? element : element[0]; - return (element.scopeName && element.scopeName != 'HTML') - ? uppercase(element.scopeName + ':' + element.nodeName) : element.nodeName; - }; -} else { - nodeName_ = function(element) { - return element.nodeName ? element.nodeName : element[0].nodeName; - }; +function nodeName_(element) { + return lowercase(element.nodeName || element[0].nodeName); } @@ -757,20 +735,11 @@ function size(obj, ownPropsOnly) { function includes(array, obj) { - return indexOf(array, obj) != -1; -} - -function indexOf(array, obj) { - if (array.indexOf) return array.indexOf(obj); - - for (var i = 0; i < array.length; i++) { - if (obj === array[i]) return i; - } - return -1; + return Array.prototype.indexOf.call(array, obj) != -1; } function arrayRemove(array, value) { - var index = indexOf(array, value); + var index = array.indexOf(value); if (index >=0) array.splice(index, 1); return value; @@ -778,10 +747,10 @@ function arrayRemove(array, value) { function isLeafNode (node) { if (node) { - switch (node.nodeName) { - case "OPTION": - case "PRE": - case "TITLE": + switch (nodeName_(node)) { + case "option": + case "pre": + case "title": return true; } } @@ -826,7 +795,7 @@ function isLeafNode (node) { </div> <script> - angular.module('copyExample') + angular.module('copyExample', []) .controller('ExampleController', ['$scope', function($scope) { $scope.master= {}; @@ -860,9 +829,11 @@ function copy(source, destination, stackSource, stackDest) { } else if (isDate(source)) { destination = new Date(source.getTime()); } else if (isRegExp(source)) { - destination = new RegExp(source.source); + destination = new RegExp(source.source, source.toString().match(/[^\/]*$/)[0]); + destination.lastIndex = source.lastIndex; } else if (isObject(source)) { - destination = copy(source, {}, stackSource, stackDest); + var emptyObject = Object.create(Object.getPrototypeOf(source)); + destination = copy(source, emptyObject, stackSource, stackDest); } } } else { @@ -873,7 +844,7 @@ function copy(source, destination, stackSource, stackDest) { stackDest = stackDest || []; if (isObject(source)) { - var index = indexOf(stackSource, source); + var index = stackSource.indexOf(source); if (index !== -1) return stackDest[index]; stackSource.push(source); @@ -893,16 +864,22 @@ function copy(source, destination, stackSource, stackDest) { } } else { var h = destination.$$hashKey; - forEach(destination, function(value, key) { - delete destination[key]; - }); + if (isArray(destination)) { + destination.length = 0; + } else { + forEach(destination, function(value, key) { + delete destination[key]; + }); + } for ( var key in source) { - result = copy(source[key], null, stackSource, stackDest); - if (isObject(source[key])) { - stackSource.push(source[key]); - stackDest.push(result); + if(source.hasOwnProperty(key)) { + result = copy(source[key], null, stackSource, stackDest); + if (isObject(source[key])) { + stackSource.push(source[key]); + stackDest.push(result); + } + destination[key] = result; } - destination[key] = result; } setHashKey(destination,h); } @@ -912,20 +889,22 @@ function copy(source, destination, stackSource, stackDest) { } /** - * Creates a shallow copy of an object, an array or a primitive + * Creates a shallow copy of an object, an array or a primitive. + * + * Assumes that there are no proto properties for objects. */ function shallowCopy(src, dst) { if (isArray(src)) { dst = dst || []; - for ( var i = 0; i < src.length; i++) { + for (var i = 0, ii = src.length; i < ii; i++) { dst[i] = src[i]; } } else if (isObject(src)) { dst = dst || {}; for (var key in src) { - if (hasOwnProperty.call(src, key) && !(key.charAt(0) === '$' && key.charAt(1) === '$')) { + if (!(key.charAt(0) === '$' && key.charAt(1) === '$')) { dst[key] = src[key]; } } @@ -980,7 +959,8 @@ function equals(o1, o2) { return true; } } else if (isDate(o1)) { - return isDate(o2) && o1.getTime() == o2.getTime(); + if (!isDate(o2)) return false; + return equals(o1.getTime(), o2.getTime()); } else if (isRegExp(o1) && isRegExp(o2)) { return o1.toString() == o2.toString(); } else { @@ -1004,12 +984,25 @@ function equals(o1, o2) { return false; } +var csp = function() { + if (isDefined(csp.isActive_)) return csp.isActive_; + + var active = !!(document.querySelector('[ng-csp]') || + document.querySelector('[data-ng-csp]')); + + if (!active) { + try { + /* jshint -W031, -W054 */ + new Function(''); + /* jshint +W031, +W054 */ + } catch (e) { + active = true; + } + } + + return (csp.isActive_ = active); +}; -function csp() { - return (document.securityPolicy && document.securityPolicy.isActive) || - (document.querySelector && - !!(document.querySelector('[ng-csp]') || document.querySelector('[data-ng-csp]'))); -} function concat(array1, array2, index) { @@ -1064,7 +1057,7 @@ function bind(self, fn) { function toJsonReplacer(key, value) { var val = value; - if (typeof key === 'string' && key.charAt(0) === '$') { + if (typeof key === 'string' && key.charAt(0) === '$' && key.charAt(1) === '$') { val = undefined; } else if (isWindow(value)) { val = '$WINDOW'; @@ -1085,7 +1078,7 @@ function toJsonReplacer(key, value) { * @kind function * * @description - * Serializes input into a JSON-formatted string. Properties with leading $ characters will be + * Serializes input into a JSON-formatted string. Properties with leading $$ characters will be * stripped since angular uses this notation internally. * * @param {Object|Array|Date|string|number} obj Input to be serialized into JSON. @@ -1117,18 +1110,6 @@ function fromJson(json) { } -function toBoolean(value) { - if (typeof value === 'function') { - value = true; - } else if (value && value.length !== 0) { - var v = lowercase("" + value); - value = !(v == 'f' || v == '0' || v == 'false' || v == 'no' || v == 'n' || v == '[]'); - } else { - value = false; - } - return value; -} - /** * @returns {string} Returns the string representation of the element. */ @@ -1181,7 +1162,7 @@ function parseKeyValue(/**string*/keyValue) { var obj = {}, key_value, key; forEach((keyValue || "").split('&'), function(keyValue) { if ( keyValue ) { - key_value = keyValue.split('='); + key_value = keyValue.replace(/\+/g,'%20').split('='); key = tryDecodeURIComponent(key_value[0]); if ( isDefined(key) ) { var val = isDefined(key_value[1]) ? tryDecodeURIComponent(key_value[1]) : true; @@ -1251,9 +1232,23 @@ function encodeUriQuery(val, pctEncodeSpaces) { replace(/%3A/gi, ':'). replace(/%24/g, '$'). replace(/%2C/gi, ','). + replace(/%3B/gi, ';'). replace(/%20/g, (pctEncodeSpaces ? '%20' : '+')); } +var ngAttrPrefixes = ['ng-', 'data-ng-', 'ng:', 'x-ng-']; + +function getNgAttribute(element, ngAttr) { + var attr, i, ii = ngAttrPrefixes.length; + element = jqLite(element); + for (i=0; i<ii; ++i) { + attr = ngAttrPrefixes[i] + ngAttr; + if (isString(attr = element.attr(attr))) { + return attr; + } + } + return null; +} /** * @ngdoc directive @@ -1263,6 +1258,11 @@ function encodeUriQuery(val, pctEncodeSpaces) { * @element ANY * @param {angular.Module} ngApp an optional application * {@link angular.module module} name to load. + * @param {boolean=} ngStrictDi if this attribute is present on the app element, the injector will be + * created in "strict-di" mode. This means that the application will fail to invoke functions which + * do not use explicit function annotation (and are thus unsuitable for minification), as described + * in {@link guide/di the Dependency Injection guide}, and useful debugging info will assist in + * tracking down the root of these bugs. * * @description * @@ -1300,48 +1300,109 @@ function encodeUriQuery(val, pctEncodeSpaces) { </file> </example> * + * Using `ngStrictDi`, you would see something like this: + * + <example ng-app-included="true"> + <file name="index.html"> + <div ng-app="ngAppStrictDemo" ng-strict-di> + <div ng-controller="GoodController1"> + I can add: {{a}} + {{b}} = {{ a+b }} + + <p>This renders because the controller does not fail to + instantiate, by using explicit annotation style (see + script.js for details) + </p> + </div> + + <div ng-controller="GoodController2"> + Name: <input ng-model="name"><br /> + Hello, {{name}}! + + <p>This renders because the controller does not fail to + instantiate, by using explicit annotation style + (see script.js for details) + </p> + </div> + + <div ng-controller="BadController"> + I can add: {{a}} + {{b}} = {{ a+b }} + + <p>The controller could not be instantiated, due to relying + on automatic function annotations (which are disabled in + strict mode). As such, the content of this section is not + interpolated, and there should be an error in your web console. + </p> + </div> + </div> + </file> + <file name="script.js"> + angular.module('ngAppStrictDemo', []) + // BadController will fail to instantiate, due to relying on automatic function annotation, + // rather than an explicit annotation + .controller('BadController', function($scope) { + $scope.a = 1; + $scope.b = 2; + }) + // Unlike BadController, GoodController1 and GoodController2 will not fail to be instantiated, + // due to using explicit annotations using the array style and $inject property, respectively. + .controller('GoodController1', ['$scope', function($scope) { + $scope.a = 1; + $scope.b = 2; + }]) + .controller('GoodController2', GoodController2); + function GoodController2($scope) { + $scope.name = "World"; + } + GoodController2.$inject = ['$scope']; + </file> + <file name="style.css"> + div[ng-controller] { + margin-bottom: 1em; + -webkit-border-radius: 4px; + border-radius: 4px; + border: 1px solid; + padding: .5em; + } + div[ng-controller^=Good] { + border-color: #d6e9c6; + background-color: #dff0d8; + color: #3c763d; + } + div[ng-controller^=Bad] { + border-color: #ebccd1; + background-color: #f2dede; + color: #a94442; + margin-bottom: 0; + } + </file> + </example> */ function angularInit(element, bootstrap) { - var elements = [element], - appElement, + var appElement, module, - names = ['ng:app', 'ng-app', 'x-ng-app', 'data-ng-app'], - NG_APP_CLASS_REGEXP = /\sng[:\-]app(:\s*([\w\d_]+);?)?\s/; + config = {}; - function append(element) { - element && elements.push(element); - } + // The element `element` has priority over any other element + forEach(ngAttrPrefixes, function(prefix) { + var name = prefix + 'app'; - forEach(names, function(name) { - names[name] = true; - append(document.getElementById(name)); - name = name.replace(':', '\\:'); - if (element.querySelectorAll) { - forEach(element.querySelectorAll('.' + name), append); - forEach(element.querySelectorAll('.' + name + '\\:'), append); - forEach(element.querySelectorAll('[' + name + ']'), append); + if (!appElement && element.hasAttribute && element.hasAttribute(name)) { + appElement = element; + module = element.getAttribute(name); } }); + forEach(ngAttrPrefixes, function(prefix) { + var name = prefix + 'app'; + var candidate; - forEach(elements, function(element) { - if (!appElement) { - var className = ' ' + element.className + ' '; - var match = NG_APP_CLASS_REGEXP.exec(className); - if (match) { - appElement = element; - module = (match[2] || '').replace(/\s+/g, ','); - } else { - forEach(element.attributes, function(attr) { - if (!appElement && names[attr.name]) { - appElement = element; - module = attr.value; - } - }); - } + if (!appElement && (candidate = element.querySelector('[' + name.replace(':', '\\:') + ']'))) { + appElement = candidate; + module = candidate.getAttribute(name); } }); if (appElement) { - bootstrap(appElement, module ? [module] : []); + config.strictDi = getNgAttribute(appElement, "strict-di") !== null; + bootstrap(appElement, module ? [module] : [], config); } } @@ -1354,7 +1415,7 @@ function angularInit(element, bootstrap) { * * See: {@link guide/bootstrap Bootstrap} * - * Note that ngScenario-based end-to-end tests cannot use this function to bootstrap manually. + * Note that Protractor based end-to-end tests cannot use this function to bootstrap manually. * They must use {@link ng.directive:ngApp ngApp}. * * Angular will detect if it has been loaded into the browser more than once and only allow the @@ -1362,60 +1423,73 @@ function angularInit(element, bootstrap) { * each of the subsequent scripts. This prevents strange results in applications, where otherwise * multiple instances of Angular try to work on the DOM. * - * <example name="multi-bootstrap" module="multi-bootstrap"> - * <file name="index.html"> - * <script src="../../../angular.js"></script> - * <div ng-controller="BrokenTable"> - * <table> - * <tr> - * <th ng-repeat="heading in headings">{{heading}}</th> - * </tr> - * <tr ng-repeat="filling in fillings"> - * <td ng-repeat="fill in filling">{{fill}}</td> - * </tr> - * </table> + * ```html + * <!doctype html> + * <html> + * <body> + * <div ng-controller="WelcomeController"> + * {{greeting}} * </div> - * </file> - * <file name="controller.js"> - * var app = angular.module('multi-bootstrap', []) * - * .controller('BrokenTable', function($scope) { - * $scope.headings = ['One', 'Two', 'Three']; - * $scope.fillings = [[1, 2, 3], ['A', 'B', 'C'], [7, 8, 9]]; - * }); - * </file> - * <file name="protractor.js" type="protractor"> - * it('should only insert one table cell for each item in $scope.fillings', function() { - * expect(element.all(by.css('td')).count()) - * .toBe(9); - * }); - * </file> - * </example> + * <script src="angular.js"></script> + * <script> + * var app = angular.module('demo', []) + * .controller('WelcomeController', function($scope) { + * $scope.greeting = 'Welcome!'; + * }); + * angular.bootstrap(document, ['demo']); + * </script> + * </body> + * </html> + * ``` * * @param {DOMElement} element DOM element which is the root of angular application. * @param {Array<String|Function|Array>=} modules an array of modules to load into the application. * Each item in the array should be the name of a predefined module or a (DI annotated) * function that will be invoked by the injector as a run block. * See: {@link angular.module modules} + * @param {Object=} config an object for defining configuration options for the application. The + * following keys are supported: + * + * - `strictDi`: disable automatic function annotation for the application. This is meant to + * assist in finding bugs which break minified code. + * * @returns {auto.$injector} Returns the newly created injector for this app. */ -function bootstrap(element, modules) { +function bootstrap(element, modules, config) { + if (!isObject(config)) config = {}; + var defaultConfig = { + strictDi: false + }; + config = extend(defaultConfig, config); var doBootstrap = function() { element = jqLite(element); if (element.injector()) { var tag = (element[0] === document) ? 'document' : startingTag(element); - throw ngMinErr('btstrpd', "App Already Bootstrapped with this Element '{0}'", tag); + //Encode angle brackets to prevent input from being sanitized to empty string #8683 + throw ngMinErr( + 'btstrpd', + "App Already Bootstrapped with this Element '{0}'", + tag.replace(/</,'<').replace(/>/,'>')); } modules = modules || []; modules.unshift(['$provide', function($provide) { $provide.value('$rootElement', element); }]); + + if (config.debugInfoEnabled) { + // Pushing so that this overrides `debugInfoEnabled` setting defined in user's `modules`. + modules.push(['$compileProvider', function($compileProvider) { + $compileProvider.debugInfoEnabled(true); + }]); + } + modules.unshift('ng'); - var injector = createInjector(modules); - injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector', '$animate', - function(scope, element, compile, injector, animate) { + var injector = createInjector(modules, config.strictDi); + injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector', + function bootstrapApply(scope, element, compile, injector) { scope.$apply(function() { element.data('$injector', injector); compile(element)(scope); @@ -1425,8 +1499,14 @@ function bootstrap(element, modules) { return injector; }; + var NG_ENABLE_DEBUG_INFO = /^NG_ENABLE_DEBUG_INFO!/; var NG_DEFER_BOOTSTRAP = /^NG_DEFER_BOOTSTRAP!/; + if (window && NG_ENABLE_DEBUG_INFO.test(window.name)) { + config.debugInfoEnabled = true; + window.name = window.name.replace(NG_ENABLE_DEBUG_INFO, ''); + } + if (window && !NG_DEFER_BOOTSTRAP.test(window.name)) { return doBootstrap(); } @@ -1440,6 +1520,33 @@ function bootstrap(element, modules) { }; } +/** + * @ngdoc function + * @name angular.reloadWithDebugInfo + * @module ng + * @description + * Use this function to reload the current application with debug information turned on. + * This takes precedence over a call to `$compileProvider.debugInfoEnabled(false)`. + * + * See {@link ng.$compileProvider#debugInfoEnabled} for more. + */ +function reloadWithDebugInfo() { + window.name = 'NG_ENABLE_DEBUG_INFO!' + window.name; + window.location.reload(); +} + +/* + * @name angular.getTestability + * @module ng + * @description + * Get the testability service for the instance of Angular on the given + * element. + * @param {DOMElement} element DOM element which is the root of angular application. + */ +function getTestability(rootElement) { + return angular.element(rootElement).injector().get('$$testability'); +} + var SNAKE_CASE_REGEXP = /[A-Z]/g; function snake_case(name, separator) { separator = separator || '_'; @@ -1448,11 +1555,21 @@ function snake_case(name, separator) { }); } +var bindJQueryFired = false; +var skipDestroyOnNextJQueryCleanData; function bindJQuery() { + var originalCleanData; + + if (bindJQueryFired) { + return; + } + // bind to jQuery if present; jQuery = window.jQuery; // Use jQuery if it exists with proper functionality, otherwise default to us. - // Angular 1.2+ requires jQuery 1.7.1+ for on()/off() support. + // Angular 1.2+ requires jQuery 1.7+ for on()/off() support. + // Angular 1.3+ technically requires at least jQuery 2.1+ but it may work with older + // versions. It will not work for sure with jQuery <1.7, though. if (jQuery && jQuery.fn.on) { jqLite = jQuery; extend(jQuery.fn, { @@ -1462,15 +1579,33 @@ function bindJQuery() { injector: JQLitePrototype.injector, inheritedData: JQLitePrototype.inheritedData }); - // Method signature: - // jqLitePatchJQueryRemove(name, dispatchThis, filterElems, getterIfNoArguments) - jqLitePatchJQueryRemove('remove', true, true, false); - jqLitePatchJQueryRemove('empty', false, false, false); - jqLitePatchJQueryRemove('html', false, false, true); + + // All nodes removed from the DOM via various jQuery APIs like .remove() + // are passed through jQuery.cleanData. Monkey-patch this method to fire + // the $destroy event on all removed nodes. + originalCleanData = jQuery.cleanData; + jQuery.cleanData = function(elems) { + var events; + if (!skipDestroyOnNextJQueryCleanData) { + for (var i = 0, elem; (elem = elems[i]) != null; i++) { + events = jQuery._data(elem, "events"); + if (events && events.$destroy) { + jQuery(elem).triggerHandler('$destroy'); + } + } + } else { + skipDestroyOnNextJQueryCleanData = false; + } + originalCleanData(elems); + }; } else { jqLite = JQLite; } + angular.element = jqLite; + + // Prevent double-proxying. + bindJQueryFired = true; } /** @@ -1534,25 +1669,38 @@ function getter(obj, path, bindFnToScope) { /** * Return the DOM siblings between the first and last node in the given array. * @param {Array} array like object - * @returns {DOMElement} object containing the elements + * @returns {jqLite} jqLite collection containing the nodes */ -function getBlockElements(nodes) { - var startNode = nodes[0], - endNode = nodes[nodes.length - 1]; - if (startNode === endNode) { - return jqLite(startNode); - } - - var element = startNode; - var elements = [element]; +function getBlockNodes(nodes) { + // TODO(perf): just check if all items in `nodes` are siblings and if they are return the original + // collection, otherwise update the original collection. + var node = nodes[0]; + var endNode = nodes[nodes.length - 1]; + var blockNodes = [node]; do { - element = element.nextSibling; - if (!element) break; - elements.push(element); - } while (element !== endNode); + node = node.nextSibling; + if (!node) break; + blockNodes.push(node); + } while (node !== endNode); - return jqLite(elements); + return jqLite(blockNodes); +} + + +/** + * Creates a new object without a prototype. This object is useful for lookup without having to + * guard against prototypically inherited properties via hasOwnProperty. + * + * Related micro-benchmarks: + * - http://jsperf.com/object-create2 + * - http://jsperf.com/proto-map-lookup/2 + * - http://jsperf.com/for-in-vs-object-keys2 + * + * @returns {Object} + */ +function createMap() { + return Object.create(null); } /** @@ -1655,21 +1803,25 @@ function setupModuleLoader(window) { var invokeQueue = []; /** @type {!Array.<Function>} */ + var configBlocks = []; + + /** @type {!Array.<Function>} */ var runBlocks = []; - var config = invokeLater('$injector', 'invoke'); + var config = invokeLater('$injector', 'invoke', 'push', configBlocks); /** @type {angular.Module} */ var moduleInstance = { // Private state _invokeQueue: invokeQueue, + _configBlocks: configBlocks, _runBlocks: runBlocks, /** * @ngdoc property * @name angular.Module#requires * @module ng - * @returns {Array.<string>} List of module names which must be loaded before this module. + * * @description * Holds the list of modules which the injector will load before the current module is * loaded. @@ -1680,8 +1832,9 @@ function setupModuleLoader(window) { * @ngdoc property * @name angular.Module#name * @module ng - * @returns {string} Name of the module. + * * @description + * Name of the module. */ name: name, @@ -1854,9 +2007,10 @@ function setupModuleLoader(window) { * @param {String=} insertMethod * @returns {angular.Module} */ - function invokeLater(provider, method, insertMethod) { + function invokeLater(provider, method, insertMethod, queue) { + if (!queue) queue = invokeQueue; return function() { - invokeQueue[insertMethod || 'push']([provider, method, arguments]); + queue[insertMethod || 'push']([provider, method, arguments]); return moduleInstance; }; } @@ -1866,81 +2020,90 @@ function setupModuleLoader(window) { } -/* global - angularModule: true, - version: true, - - $LocaleProvider, - $CompileProvider, - - htmlAnchorDirective, - inputDirective, - inputDirective, - formDirective, - scriptDirective, - selectDirective, - styleDirective, - optionDirective, - ngBindDirective, - ngBindHtmlDirective, - ngBindTemplateDirective, - ngClassDirective, - ngClassEvenDirective, - ngClassOddDirective, - ngCspDirective, - ngCloakDirective, - ngControllerDirective, - ngFormDirective, - ngHideDirective, - ngIfDirective, - ngIncludeDirective, - ngIncludeFillContentDirective, - ngInitDirective, - ngNonBindableDirective, - ngPluralizeDirective, - ngRepeatDirective, - ngShowDirective, - ngStyleDirective, - ngSwitchDirective, - ngSwitchWhenDirective, - ngSwitchDefaultDirective, - ngOptionsDirective, - ngTranscludeDirective, - ngModelDirective, - ngListDirective, - ngChangeDirective, - requiredDirective, - requiredDirective, - ngValueDirective, - ngAttributeAliasDirectives, - ngEventDirectives, - - $AnchorScrollProvider, - $AnimateProvider, - $BrowserProvider, - $CacheFactoryProvider, - $ControllerProvider, - $DocumentProvider, - $ExceptionHandlerProvider, - $FilterProvider, - $InterpolateProvider, - $IntervalProvider, - $HttpProvider, - $HttpBackendProvider, - $LocationProvider, - $LogProvider, - $ParseProvider, - $RootScopeProvider, - $QProvider, - $$SanitizeUriProvider, - $SceProvider, - $SceDelegateProvider, - $SnifferProvider, - $TemplateCacheProvider, - $TimeoutProvider, - $$RAFProvider, - $$AsyncCallbackProvider, - $WindowProvider +/* global angularModule: true, + version: true, + + $LocaleProvider, + $CompileProvider, + + htmlAnchorDirective, + inputDirective, + inputDirective, + formDirective, + scriptDirective, + selectDirective, + styleDirective, + optionDirective, + ngBindDirective, + ngBindHtmlDirective, + ngBindTemplateDirective, + ngClassDirective, + ngClassEvenDirective, + ngClassOddDirective, + ngCspDirective, + ngCloakDirective, + ngControllerDirective, + ngFormDirective, + ngHideDirective, + ngIfDirective, + ngIncludeDirective, + ngIncludeFillContentDirective, + ngInitDirective, + ngNonBindableDirective, + ngPluralizeDirective, + ngRepeatDirective, + ngShowDirective, + ngStyleDirective, + ngSwitchDirective, + ngSwitchWhenDirective, + ngSwitchDefaultDirective, + ngOptionsDirective, + ngTranscludeDirective, + ngModelDirective, + ngListDirective, + ngChangeDirective, + patternDirective, + patternDirective, + requiredDirective, + requiredDirective, + minlengthDirective, + minlengthDirective, + maxlengthDirective, + maxlengthDirective, + ngValueDirective, + ngModelOptionsDirective, + ngAttributeAliasDirectives, + ngEventDirectives, + + $AnchorScrollProvider, + $AnimateProvider, + $BrowserProvider, + $CacheFactoryProvider, + $ControllerProvider, + $DocumentProvider, + $ExceptionHandlerProvider, + $FilterProvider, + $InterpolateProvider, + $IntervalProvider, + $HttpProvider, + $HttpBackendProvider, + $LocationProvider, + $LogProvider, + $ParseProvider, + $RootScopeProvider, + $QProvider, + $$QProvider, + $$SanitizeUriProvider, + $SceProvider, + $SceDelegateProvider, + $SnifferProvider, + $TemplateCacheProvider, + $TemplateRequestProvider, + $$TestabilityProvider, + $TimeoutProvider, + $$RAFProvider, + $$AsyncCallbackProvider, + $WindowProvider */ @@ -1959,11 +2122,11 @@ function setupModuleLoader(window) { * - `codeName` – `{string}` – Code name of the release, such as "jiggling-armfat". */ var version = { - full: '1.2.20', // all of these placeholder strings will be replaced by grunt's + full: '1.3.0-rc.1', // all of these placeholder strings will be replaced by grunt's major: 1, // package task - minor: 2, - dot: 20, - codeName: 'accidental-beautification' + minor: 3, + dot: 0, + codeName: 'backyard-atomicity' }; @@ -1976,11 +2139,11 @@ function publishExternalAPI(angular){ 'element': jqLite, 'forEach': forEach, 'injector': createInjector, - 'noop':noop, - 'bind':bind, + 'noop': noop, + 'bind': bind, 'toJson': toJson, 'fromJson': fromJson, - 'identity':identity, + 'identity': identity, 'isUndefined': isUndefined, 'isDefined': isDefined, 'isString': isString, @@ -1994,8 +2157,11 @@ function publishExternalAPI(angular){ 'lowercase': lowercase, 'uppercase': uppercase, 'callbacks': {counter: 0}, + 'getTestability': getTestability, '$$minErr': minErr, - '$$csp': csp + '$$csp': csp, + 'reloadWithDebugInfo': reloadWithDebugInfo, + '$$hasClass': jqLiteHasClass }); angularModule = setupModuleLoader(window); @@ -2047,9 +2213,16 @@ function publishExternalAPI(angular){ ngModel: ngModelDirective, ngList: ngListDirective, ngChange: ngChangeDirective, + pattern: patternDirective, + ngPattern: patternDirective, required: requiredDirective, ngRequired: requiredDirective, - ngValue: ngValueDirective + minlength: minlengthDirective, + ngMinlength: minlengthDirective, + maxlength: maxlengthDirective, + ngMaxlength: maxlengthDirective, + ngValue: ngValueDirective, + ngModelOptions: ngModelOptionsDirective }). directive({ ngInclude: ngIncludeFillContentDirective @@ -2074,10 +2247,13 @@ function publishExternalAPI(angular){ $parse: $ParseProvider, $rootScope: $RootScopeProvider, $q: $QProvider, + $$q: $$QProvider, $sce: $SceProvider, $sceDelegate: $SceDelegateProvider, $sniffer: $SnifferProvider, $templateCache: $TemplateCacheProvider, + $templateRequest: $TemplateRequestProvider, + $$testability: $$TestabilityProvider, $timeout: $TimeoutProvider, $window: $WindowProvider, $$rAF: $$RAFProvider, @@ -2087,12 +2263,11 @@ function publishExternalAPI(angular){ ]); } -/* global - - -JQLitePrototype, - -addEventListenerFn, - -removeEventListenerFn, - -BOOLEAN_ATTR +/* global JQLitePrototype: true, + addEventListenerFn: true, + removeEventListenerFn: true, + BOOLEAN_ATTR: true, + ALIASED_ATTR: true, */ ////////////////////////////////// @@ -2134,6 +2309,7 @@ function publishExternalAPI(angular){ * - [`contents()`](http://api.jquery.com/contents/) * - [`css()`](http://api.jquery.com/css/) * - [`data()`](http://api.jquery.com/data/) + * - [`detach()`](http://api.jquery.com/detach/) * - [`empty()`](http://api.jquery.com/empty/) * - [`eq()`](http://api.jquery.com/eq/) * - [`find()`](http://api.jquery.com/find/) - Limited to lookups by tag name @@ -2189,17 +2365,17 @@ JQLite.expando = 'ng339'; var jqCache = JQLite.cache = {}, jqId = 1, - addEventListenerFn = (window.document.addEventListener - ? function(element, type, fn) {element.addEventListener(type, fn, false);} - : function(element, type, fn) {element.attachEvent('on' + type, fn);}), - removeEventListenerFn = (window.document.removeEventListener - ? function(element, type, fn) {element.removeEventListener(type, fn, false); } - : function(element, type, fn) {element.detachEvent('on' + type, fn); }); + addEventListenerFn = function(element, type, fn) { + element.addEventListener(type, fn, false); + }, + removeEventListenerFn = function(element, type, fn) { + element.removeEventListener(type, fn, false); + }; /* * !!! This is an undocumented "private" function !!! */ -var jqData = JQLite._data = function(node) { +JQLite._data = function(node) { //jQuery always returns an object on cache miss return this.cache[node[this.expando]] || {}; }; @@ -2209,6 +2385,7 @@ function jqNextId() { return ++jqId; } var SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g; var MOZ_HACK_REGEXP = /^moz([A-Z])/; +var MOUSE_EVENT_MAP= { mouseleave : "mouseout", mouseenter : "mouseover"}; var jqLiteMinErr = minErr('jqLite'); /** @@ -2224,49 +2401,6 @@ function camelCase(name) { replace(MOZ_HACK_REGEXP, 'Moz$1'); } -///////////////////////////////////////////// -// jQuery mutation patch -// -// In conjunction with bindJQuery intercepts all jQuery's DOM destruction apis and fires a -// $destroy event on all DOM nodes being removed. -// -///////////////////////////////////////////// - -function jqLitePatchJQueryRemove(name, dispatchThis, filterElems, getterIfNoArguments) { - var originalJqFn = jQuery.fn[name]; - originalJqFn = originalJqFn.$original || originalJqFn; - removePatch.$original = originalJqFn; - jQuery.fn[name] = removePatch; - - function removePatch(param) { - // jshint -W040 - var list = filterElems && param ? [this.filter(param)] : [this], - fireEvent = dispatchThis, - set, setIndex, setLength, - element, childIndex, childLength, children; - - if (!getterIfNoArguments || param != null) { - while(list.length) { - set = list.shift(); - for(setIndex = 0, setLength = set.length; setIndex < setLength; setIndex++) { - element = jqLite(set[setIndex]); - if (fireEvent) { - element.triggerHandler('$destroy'); - } else { - fireEvent = !fireEvent; - } - for(childIndex = 0, childLength = (children = element.children()).length; - childIndex < childLength; - childIndex++) { - list.push(jQuery(children[childIndex])); - } - } - } - } - return originalJqFn.apply(this, arguments); - } -} - var SINGLE_TAG_REGEXP = /^<(\w+)\s*\/?>(?:<\/\1>|)$/; var HTML_REGEXP = /<|&#?\w+;/; var TAG_NAME_REGEXP = /<([\w:]+)/; @@ -2286,26 +2420,32 @@ wrapMap.optgroup = wrapMap.option; wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; wrapMap.th = wrapMap.td; + function jqLiteIsTextNode(html) { return !HTML_REGEXP.test(html); } +function jqLiteAcceptsData(node) { + // The window object can accept data but has no nodeType + // Otherwise we are only interested in elements (1) and documents (9) + var nodeType = node.nodeType; + return nodeType === 1 || !nodeType || nodeType === 9; +} + function jqLiteBuildFragment(html, context) { - var elem, tmp, tag, wrap, + var tmp, tag, wrap, fragment = context.createDocumentFragment(), - nodes = [], i, j, jj; + nodes = [], i; if (jqLiteIsTextNode(html)) { // Convert non-html into a text node nodes.push(context.createTextNode(html)); } else { - tmp = fragment.appendChild(context.createElement('div')); // Convert html into DOM nodes + tmp = tmp || fragment.appendChild(context.createElement("div")); tag = (TAG_NAME_REGEXP.exec(html) || ["", ""])[1].toLowerCase(); wrap = wrapMap[tag] || wrapMap._default; - tmp.innerHTML = '<div> </div>' + - wrap[1] + html.replace(XHTML_TAG_REGEXP, "<$1></$2>") + wrap[2]; - tmp.removeChild(tmp.firstChild); + tmp.innerHTML = wrap[1] + html.replace(XHTML_TAG_REGEXP, "<$1></$2>") + wrap[2]; // Descend through wrappers to the right content i = wrap[0]; @@ -2313,7 +2453,7 @@ function jqLiteBuildFragment(html, context) { tmp = tmp.lastChild; } - for (j=0, jj=tmp.childNodes.length; j<jj; ++j) nodes.push(tmp.childNodes[j]); + nodes = concat(nodes, tmp.childNodes); tmp = fragment.firstChild; tmp.textContent = ""; @@ -2322,7 +2462,11 @@ function jqLiteBuildFragment(html, context) { // Remove wrapper from fragment fragment.textContent = ""; fragment.innerHTML = ""; // Clear inner HTML - return nodes; + forEach(nodes, function(node) { + fragment.appendChild(node); + }); + + return fragment; } function jqLiteParseHTML(html, context) { @@ -2333,7 +2477,11 @@ function jqLiteParseHTML(html, context) { return [context.createElement(parsed[1])]; } - return jqLiteBuildFragment(html, context); + if ((parsed = jqLiteBuildFragment(html, context))) { + return parsed.childNodes; + } + + return []; } ///////////////////////////////////////////// @@ -2341,20 +2489,22 @@ function JQLite(element) { if (element instanceof JQLite) { return element; } + + var argIsString; + if (isString(element)) { element = trim(element); + argIsString = true; } if (!(this instanceof JQLite)) { - if (isString(element) && element.charAt(0) != '<') { + if (argIsString && element.charAt(0) != '<') { throw jqLiteMinErr('nosel', 'Looking up elements via selectors is not supported by jqLite! See: http://docs.angularjs.org/api/angular.element'); } return new JQLite(element); } - if (isString(element)) { + if (argIsString) { jqLiteAddNodes(this, jqLiteParseHTML(element)); - var fragment = jqLite(document.createDocumentFragment()); - fragment.append(this); } else { jqLiteAddNodes(this, element); } @@ -2364,26 +2514,33 @@ function jqLiteClone(element) { return element.cloneNode(true); } -function jqLiteDealoc(element){ - jqLiteRemoveData(element); - for ( var i = 0, children = element.childNodes || []; i < children.length; i++) { - jqLiteDealoc(children[i]); +function jqLiteDealoc(element, onlyDescendants){ + if (!onlyDescendants) jqLiteRemoveData(element); + + if (element.querySelectorAll) { + var descendants = element.querySelectorAll('*'); + for (var i = 0, l = descendants.length; i < l; i++) { + jqLiteRemoveData(descendants[i]); + } } } function jqLiteOff(element, type, fn, unsupported) { if (isDefined(unsupported)) throw jqLiteMinErr('offargs', 'jqLite#off() does not support the `selector` argument'); - var events = jqLiteExpandoStore(element, 'events'), - handle = jqLiteExpandoStore(element, 'handle'); + var expandoStore = jqLiteExpandoStore(element); + var events = expandoStore && expandoStore.events; + var handle = expandoStore && expandoStore.handle; if (!handle) return; //no listeners registered - if (isUndefined(type)) { - forEach(events, function(eventHandler, type) { - removeEventListenerFn(element, type, eventHandler); + if (!type) { + for (type in events) { + if (type !== '$destroy') { + removeEventListenerFn(element, type, events[type]); + } delete events[type]; - }); + } } else { forEach(type.split(' '), function(type) { if (isUndefined(fn)) { @@ -2397,17 +2554,19 @@ function jqLiteOff(element, type, fn, unsupported) { } function jqLiteRemoveData(element, name) { - var expandoId = element.ng339, - expandoStore = jqCache[expandoId]; + var expandoId = element.ng339; + var expandoStore = expandoId && jqCache[expandoId]; if (expandoStore) { if (name) { - delete jqCache[expandoId].data[name]; + delete expandoStore.data[name]; return; } if (expandoStore.handle) { - expandoStore.events.$destroy && expandoStore.handle({}, '$destroy'); + if (expandoStore.events.$destroy) { + expandoStore.handle({}, '$destroy'); + } jqLiteOff(element); } delete jqCache[expandoId]; @@ -2415,43 +2574,42 @@ function jqLiteRemoveData(element, name) { } } -function jqLiteExpandoStore(element, key, value) { + +function jqLiteExpandoStore(element, createIfNecessary) { var expandoId = element.ng339, - expandoStore = jqCache[expandoId || -1]; + expandoStore = expandoId && jqCache[expandoId]; - if (isDefined(value)) { - if (!expandoStore) { - element.ng339 = expandoId = jqNextId(); - expandoStore = jqCache[expandoId] = {}; - } - expandoStore[key] = value; - } else { - return expandoStore && expandoStore[key]; + if (createIfNecessary && !expandoStore) { + element.ng339 = expandoId = jqNextId(); + expandoStore = jqCache[expandoId] = {events: {}, data: {}, handle: undefined}; } + + return expandoStore; } + function jqLiteData(element, key, value) { - var data = jqLiteExpandoStore(element, 'data'), - isSetter = isDefined(value), - keyDefined = !isSetter && isDefined(key), - isSimpleGetter = keyDefined && !isObject(key); + if (jqLiteAcceptsData(element)) { - if (!data && !isSimpleGetter) { - jqLiteExpandoStore(element, 'data', data = {}); - } + var isSimpleSetter = isDefined(value); + var isSimpleGetter = !isSimpleSetter && key && !isObject(key); + var massGetter = !key; + var expandoStore = jqLiteExpandoStore(element, !isSimpleGetter); + var data = expandoStore && expandoStore.data; - if (isSetter) { - data[key] = value; - } else { - if (keyDefined) { - if (isSimpleGetter) { - // don't create data in this case. - return data && data[key]; + if (isSimpleSetter) { // data('key', value) + data[key] = value; + } else { + if (massGetter) { // data() + return data; } else { - extend(data, key); + if (isSimpleGetter) { // data('key') + // don't force creation of expandoStore if it doesn't exist yet + return data && data[key]; + } else { // mass-setter: data({key1: val1, key2: val2}) + extend(data, key); + } } - } else { - return data; } } } @@ -2490,53 +2648,70 @@ function jqLiteAddClass(element, cssClasses) { } } + function jqLiteAddNodes(root, elements) { + // THIS CODE IS VERY HOT. Don't make changes without benchmarking. + if (elements) { - elements = (!elements.nodeName && isDefined(elements.length) && !isWindow(elements)) - ? elements - : [ elements ]; - for(var i=0; i < elements.length; i++) { - root.push(elements[i]); + + // if a Node (the most common case) + if (elements.nodeType) { + root[root.length++] = elements; + } else { + var length = elements.length; + + // if an Array or NodeList and not a Window + if (typeof length === 'number' && elements.window !== elements) { + if (length) { + for (var i = 0; i < length; i++) { + root[root.length++] = elements[i]; + } + } + } else { + root[root.length++] = elements; + } } } } + function jqLiteController(element, name) { return jqLiteInheritedData(element, '$' + (name || 'ngController' ) + 'Controller'); } function jqLiteInheritedData(element, name, value) { - element = jqLite(element); - // if element is the document object work with the html element instead // this makes $(document).scope() possible - if(element[0].nodeType == 9) { - element = element.find('html'); + if(element.nodeType == 9) { + element = element.documentElement; } var names = isArray(name) ? name : [name]; - while (element.length) { - var node = element[0]; + while (element) { for (var i = 0, ii = names.length; i < ii; i++) { - if ((value = element.data(names[i])) !== undefined) return value; + if ((value = jqLite.data(element, names[i])) !== undefined) return value; } // If dealing with a document fragment node with a host element, and no parent, use the host // element as the parent. This enables directives within a Shadow DOM or polyfilled Shadow DOM // to lookup parent controllers. - element = jqLite(node.parentNode || (node.nodeType === 11 && node.host)); + element = element.parentNode || (element.nodeType === 11 && element.host); } } function jqLiteEmpty(element) { - for (var i = 0, childNodes = element.childNodes; i < childNodes.length; i++) { - jqLiteDealoc(childNodes[i]); - } + jqLiteDealoc(element, true); while (element.firstChild) { element.removeChild(element.firstChild); } } +function jqLiteRemove(element, keepData) { + if (!keepData) jqLiteDealoc(element); + var parent = element.parentNode; + if (parent) parent.removeChild(element); +} + ////////////////////////////////////////// // Functions which are declared directly. ////////////////////////////////////////// @@ -2550,7 +2725,7 @@ var JQLitePrototype = JQLite.prototype = { fn(); } - // check if document already is loaded + // check if document is already loaded if (document.readyState === 'complete'){ setTimeout(trigger); } else { @@ -2559,6 +2734,7 @@ var JQLitePrototype = JQLite.prototype = { // jshint -W064 JQLite(window).on('load', trigger); // fallback to window.onload for others // jshint +W064 + this.on('DOMContentLoaded', trigger); } }, toString: function() { @@ -2588,29 +2764,48 @@ forEach('multiple,selected,checked,disabled,readOnly,required,open'.split(','), }); var BOOLEAN_ELEMENTS = {}; forEach('input,select,option,textarea,button,form,details'.split(','), function(value) { - BOOLEAN_ELEMENTS[uppercase(value)] = true; + BOOLEAN_ELEMENTS[value] = true; }); +var ALIASED_ATTR = { + 'ngMinlength' : 'minlength', + 'ngMaxlength' : 'maxlength', + 'ngMin' : 'min', + 'ngMax' : 'max', + 'ngPattern' : 'pattern' +}; function getBooleanAttrName(element, name) { // check dom last since we will most likely fail on name var booleanAttr = BOOLEAN_ATTR[name.toLowerCase()]; // booleanAttr is here twice to minimize DOM access - return booleanAttr && BOOLEAN_ELEMENTS[element.nodeName] && booleanAttr; + return booleanAttr && BOOLEAN_ELEMENTS[nodeName_(element)] && booleanAttr; +} + +function getAliasedAttrName(element, name) { + var nodeName = element.nodeName; + return (nodeName === 'INPUT' || nodeName === 'TEXTAREA') && ALIASED_ATTR[name]; } forEach({ data: jqLiteData, + removeData: jqLiteRemoveData +}, function(fn, name) { + JQLite[name] = fn; +}); + +forEach({ + data: jqLiteData, inheritedData: jqLiteInheritedData, scope: function(element) { // Can't use jqLiteData here directly so we stay compatible with jQuery! - return jqLite(element).data('$scope') || jqLiteInheritedData(element.parentNode || element, ['$isolateScope', '$scope']); + return jqLite.data(element, '$scope') || jqLiteInheritedData(element.parentNode || element, ['$isolateScope', '$scope']); }, isolateScope: function(element) { // Can't use jqLiteData here directly so we stay compatible with jQuery! - return jqLite(element).data('$isolateScope') || jqLite(element).data('$isolateScopeNoTemplate'); + return jqLite.data(element, '$isolateScope') || jqLite.data(element, '$isolateScopeNoTemplate'); }, controller: jqLiteController, @@ -2619,7 +2814,7 @@ forEach({ return jqLiteInheritedData(element, '$injector'); }, - removeAttr: function(element,name) { + removeAttr: function(element, name) { element.removeAttribute(name); }, @@ -2631,22 +2826,7 @@ forEach({ if (isDefined(value)) { element.style[name] = value; } else { - var val; - - if (msie <= 8) { - // this is some IE specific weirdness that jQuery 1.6.4 does not sure why - val = element.currentStyle && element.currentStyle[name]; - if (val === '') val = 'auto'; - } - - val = val || element.style[name]; - - if (msie <= 8) { - // jquery weirdness :-/ - val = (val === '') ? undefined : val; - } - - return val; + return element.style[name]; } }, @@ -2687,29 +2867,21 @@ forEach({ }, text: (function() { - var NODE_TYPE_TEXT_PROPERTY = []; - if (msie < 9) { - NODE_TYPE_TEXT_PROPERTY[1] = 'innerText'; /** Element **/ - NODE_TYPE_TEXT_PROPERTY[3] = 'nodeValue'; /** Text **/ - } else { - NODE_TYPE_TEXT_PROPERTY[1] = /** Element **/ - NODE_TYPE_TEXT_PROPERTY[3] = 'textContent'; /** Text **/ - } getText.$dv = ''; return getText; function getText(element, value) { - var textProp = NODE_TYPE_TEXT_PROPERTY[element.nodeType]; if (isUndefined(value)) { - return textProp ? element[textProp] : ''; + var nodeType = element.nodeType; + return (nodeType === 1 || nodeType === 3) ? element.textContent : ''; } - element[textProp] = value; + element.textContent = value; } })(), val: function(element, value) { if (isUndefined(value)) { - if (nodeName_(element) === 'SELECT' && element.multiple) { + if (element.multiple && nodeName_(element) === 'select') { var result = []; forEach(element.options, function (option) { if (option.selected) { @@ -2727,9 +2899,7 @@ forEach({ if (isUndefined(value)) { return element.innerHTML; } - for (var i = 0, childNodes = element.childNodes; i < childNodes.length; i++) { - jqLiteDealoc(childNodes[i]); - } + jqLiteDealoc(element, true); element.innerHTML = value; }, @@ -2787,56 +2957,29 @@ forEach({ function createEventHandler(element, events) { var eventHandler = function (event, type) { - if (!event.preventDefault) { - event.preventDefault = function() { - event.returnValue = false; //ie - }; - } - - if (!event.stopPropagation) { - event.stopPropagation = function() { - event.cancelBubble = true; //ie - }; - } - - if (!event.target) { - event.target = event.srcElement || document; - } - - if (isUndefined(event.defaultPrevented)) { - var prevent = event.preventDefault; - event.preventDefault = function() { - event.defaultPrevented = true; - prevent.call(event); - }; - event.defaultPrevented = false; - } + // jQuery specific api event.isDefaultPrevented = function() { - return event.defaultPrevented || event.returnValue === false; + return event.defaultPrevented; }; - // Copy event handlers in case event handlers array is modified during execution. - var eventHandlersCopy = shallowCopy(events[type || event.type] || []); + var eventFns = events[type || event.type]; + var eventFnsLength = eventFns ? eventFns.length : 0; - forEach(eventHandlersCopy, function(fn) { - fn.call(element, event); - }); + if (!eventFnsLength) return; - // Remove monkey-patched methods (IE), - // as they would cause memory leaks in IE8. - if (msie <= 8) { - // IE7/8 does not allow to delete property on native object - event.preventDefault = null; - event.stopPropagation = null; - event.isDefaultPrevented = null; - } else { - // It shouldn't affect normal browsers (native methods are defined on prototype). - delete event.preventDefault; - delete event.stopPropagation; - delete event.isDefaultPrevented; + // Copy event handlers in case event handlers array is modified during execution. + if ((eventFnsLength > 1)) { + eventFns = shallowCopy(eventFns); + } + + for (var i = 0; i < eventFnsLength; i++) { + eventFns[i].call(element, event); } }; + + // TODO: this is a hack for angularMocks/clearDataCache that makes it possible to deregister all + // events on `element` eventHandler.elem = element; return eventHandler; } @@ -2849,68 +2992,56 @@ function createEventHandler(element, events) { forEach({ removeData: jqLiteRemoveData, - dealoc: jqLiteDealoc, - - on: function onFn(element, type, fn, unsupported){ + on: function jqLiteOn(element, type, fn, unsupported){ if (isDefined(unsupported)) throw jqLiteMinErr('onargs', 'jqLite#on() does not support the `selector` or `eventData` parameters'); - var events = jqLiteExpandoStore(element, 'events'), - handle = jqLiteExpandoStore(element, 'handle'); + // Do not add event handlers to non-elements because they will not be cleaned up. + if (!jqLiteAcceptsData(element)) { + return; + } + + var expandoStore = jqLiteExpandoStore(element, true); + var events = expandoStore.events; + var handle = expandoStore.handle; + + if (!handle) { + handle = expandoStore.handle = createEventHandler(element, events); + } - if (!events) jqLiteExpandoStore(element, 'events', events = {}); - if (!handle) jqLiteExpandoStore(element, 'handle', handle = createEventHandler(element, events)); + // http://jsperf.com/string-indexof-vs-split + var types = type.indexOf(' ') >= 0 ? type.split(' ') : [type]; + var i = types.length; - forEach(type.split(' '), function(type){ + while (i--) { + type = types[i]; var eventFns = events[type]; if (!eventFns) { - if (type == 'mouseenter' || type == 'mouseleave') { - var contains = document.body.contains || document.body.compareDocumentPosition ? - function( a, b ) { - // jshint bitwise: false - var adown = a.nodeType === 9 ? a.documentElement : a, - bup = b && b.parentNode; - return a === bup || !!( bup && bup.nodeType === 1 && ( - adown.contains ? - adown.contains( bup ) : - a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 - )); - } : - function( a, b ) { - if ( b ) { - while ( (b = b.parentNode) ) { - if ( b === a ) { - return true; - } - } - } - return false; - }; - - events[type] = []; + events[type] = []; + if (type === 'mouseenter' || type === 'mouseleave') { // Refer to jQuery's implementation of mouseenter & mouseleave // Read about mouseenter and mouseleave: // http://www.quirksmode.org/js/events_mouse.html#link8 - var eventmap = { mouseleave : "mouseout", mouseenter : "mouseover"}; - onFn(element, eventmap[type], function(event) { + jqLiteOn(element, MOUSE_EVENT_MAP[type], function(event) { var target = this, related = event.relatedTarget; // For mousenter/leave call the handler if related is outside the target. // NB: No relatedTarget if the mouse left/entered the browser window - if ( !related || (related !== target && !contains(target, related)) ){ + if ( !related || (related !== target && !target.contains(related)) ){ handle(event, type); } }); } else { - addEventListenerFn(element, type, handle); - events[type] = []; + if (type !== '$destroy') { + addEventListenerFn(element, type, handle); + } } eventFns = events[type]; } eventFns.push(fn); - }); + } }, off: jqLiteOff, @@ -2955,11 +3086,15 @@ forEach({ }, append: function(element, node) { - forEach(new JQLite(node), function(child){ - if (element.nodeType === 1 || element.nodeType === 11) { - element.appendChild(child); - } - }); + var nodeType = element.nodeType; + if (nodeType !== 1 && nodeType !== 11) return; + + node = new JQLite(node); + + for (var i = 0, ii = node.length; i < ii; i++) { + var child = node[i]; + element.appendChild(child); + } }, prepend: function(element, node) { @@ -2972,7 +3107,7 @@ forEach({ }, wrap: function(element, wrapNode) { - wrapNode = jqLite(wrapNode)[0]; + wrapNode = jqLite(wrapNode).eq(0).clone()[0]; var parent = element.parentNode; if (parent) { parent.replaceChild(wrapNode, element); @@ -2980,18 +3115,21 @@ forEach({ wrapNode.appendChild(element); }, - remove: function(element) { - jqLiteDealoc(element); - var parent = element.parentNode; - if (parent) parent.removeChild(element); + remove: jqLiteRemove, + + detach: function(element) { + jqLiteRemove(element, true); }, after: function(element, newElement) { var index = element, parent = element.parentNode; - forEach(new JQLite(newElement), function(node){ + newElement = new JQLite(newElement); + + for (var i = 0, ii = newElement.length; i < ii; i++) { + var node = newElement[i]; parent.insertBefore(node, index.nextSibling); index = node; - }); + } }, addClass: jqLiteAddClass, @@ -3015,16 +3153,7 @@ forEach({ }, next: function(element) { - if (element.nextElementSibling) { - return element.nextElementSibling; - } - - // IE8 doesn't have nextElementSibling - var elm = element.nextSibling; - while (elm != null && elm.nodeType !== 1) { - elm = elm.nextSibling; - } - return elm; + return element.nextElementSibling; }, find: function(element, selector) { @@ -3037,19 +3166,39 @@ forEach({ clone: jqLiteClone, - triggerHandler: function(element, eventName, eventData) { - var eventFns = (jqLiteExpandoStore(element, 'events') || {})[eventName]; + triggerHandler: function(element, event, extraParameters) { - eventData = eventData || []; + var dummyEvent, eventFnsCopy, handlerArgs; + var eventName = event.type || event; + var expandoStore = jqLiteExpandoStore(element); + var events = expandoStore && expandoStore.events; + var eventFns = events && events[eventName]; - var event = [{ - preventDefault: noop, - stopPropagation: noop - }]; + if (eventFns) { - forEach(eventFns, function(fn) { - fn.apply(element, event.concat(eventData)); - }); + // Create a dummy event to pass to the handlers + dummyEvent = { + preventDefault: function() { this.defaultPrevented = true; }, + isDefaultPrevented: function() { return this.defaultPrevented === true; }, + stopPropagation: noop, + type: eventName, + target: element + }; + + // If a custom event was provided then extend our dummy event with it + if (event.type) { + dummyEvent = extend(dummyEvent, event); + } + + // Copy event handlers in case event handlers array is modified during execution. + eventFnsCopy = shallowCopy(eventFns); + handlerArgs = extraParameters ? [dummyEvent].concat(extraParameters) : [dummyEvent]; + + forEach(eventFnsCopy, function(fn) { + fn.apply(element, handlerArgs); + }); + + } } }, function(fn, name){ /** @@ -3057,7 +3206,8 @@ forEach({ */ JQLite.prototype[name] = function(arg1, arg2, arg3) { var value; - for(var i=0; i < this.length; i++) { + + for(var i = 0, ii = this.length; i < ii; i++) { if (isUndefined(value)) { value = fn(this[i], arg1, arg2, arg3); if (isDefined(value)) { @@ -3089,21 +3239,23 @@ forEach({ * The resulting string key is in 'type:hashKey' format. */ function hashKey(obj, nextUidFn) { - var objType = typeof obj, - key; + var key = obj && obj.$$hashKey; - if (objType == 'function' || (objType == 'object' && obj !== null)) { - if (typeof (key = obj.$$hashKey) == 'function') { - // must invoke on object to keep the right this + if (key) { + if (typeof key === 'function') { key = obj.$$hashKey(); - } else if (key === undefined) { - key = obj.$$hashKey = (nextUidFn || nextUid)(); } + return key; + } + + var objType = typeof obj; + if (objType == 'function' || (objType == 'object' && obj !== null)) { + key = obj.$$hashKey = objType + ':' + (nextUidFn || nextUid)(); } else { - key = obj; + key = objType + ':' + obj; } - return objType + ':' + key; + return key; } /** @@ -3170,7 +3322,7 @@ HashMap.prototype = { * * // use the injector to kick off your application * // use the type inference to auto inject arguments, or use implicit injection - * $injector.invoke(function($rootScope, $compile, $document){ + * $injector.invoke(function($rootScope, $compile, $document) { * $compile($document)($rootScope); * $rootScope.$digest(); * }); @@ -3213,7 +3365,19 @@ var FN_ARG_SPLIT = /,/; var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/; var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg; var $injectorMinErr = minErr('$injector'); -function annotate(fn) { + +function anonFn(fn) { + // For anonymous functions, showing at the very least the function signature can help in + // debugging. + var fnText = fn.toString().replace(STRIP_COMMENTS, ''), + args = fnText.match(FN_ARGS); + if (args) { + return 'function(' + (args[1] || '').replace(/[\s\r\n]+/, ' ') + ')'; + } + return 'fn'; +} + +function annotate(fn, strictDi, name) { var $inject, fnText, argDecl, @@ -3223,10 +3387,17 @@ function annotate(fn) { if (!($inject = fn.$inject)) { $inject = []; if (fn.length) { + if (strictDi) { + if (!isString(name) || !name) { + name = fn.name || anonFn(fn); + } + throw $injectorMinErr('strictdi', + '{0} is not using explicit annotation and cannot be invoked in strict mode', name); + } fnText = fn.toString().replace(STRIP_COMMENTS, ''); argDecl = fnText.match(FN_ARGS); - forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg){ - arg.replace(FN_ARG, function(all, underscore, name){ + forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg) { + arg.replace(FN_ARG, function(all, underscore, name) { $inject.push(name); }); }); @@ -3261,7 +3432,7 @@ function annotate(fn) { * ```js * var $injector = angular.injector(); * expect($injector.get('$injector')).toBe($injector); - * expect($injector.invoke(function($injector){ + * expect($injector.invoke(function($injector) { * return $injector; * }).toBe($injector); * ``` @@ -3734,7 +3905,8 @@ function annotate(fn) { */ -function createInjector(modulesToLoad) { +function createInjector(modulesToLoad, strictDi) { + strictDi = (strictDi === true); var INSTANTIATING = {}, providerSuffix = 'Provider', path = [], @@ -3757,7 +3929,7 @@ function createInjector(modulesToLoad) { instanceInjector = (instanceCache.$injector = createInternalInjector(instanceCache, function(servicename) { var provider = providerInjector.get(servicename + providerSuffix); - return instanceInjector.invoke(provider.$get, provider); + return instanceInjector.invoke(provider.$get, provider, undefined, servicename); })); @@ -3820,22 +3992,27 @@ function createInjector(modulesToLoad) { // Module Loading //////////////////////////////////// function loadModules(modulesToLoad){ - var runBlocks = [], moduleFn, invokeQueue, i, ii; + var runBlocks = [], moduleFn; forEach(modulesToLoad, function(module) { if (loadedModules.get(module)) return; loadedModules.put(module, true); + function runInvokeQueue(queue) { + var i, ii; + for(i = 0, ii = queue.length; i < ii; i++) { + var invokeArgs = queue[i], + provider = providerInjector.get(invokeArgs[0]); + + provider[invokeArgs[1]].apply(provider, invokeArgs[2]); + } + } + try { if (isString(module)) { moduleFn = angularModule(module); runBlocks = runBlocks.concat(loadModules(moduleFn.requires)).concat(moduleFn._runBlocks); - - for(invokeQueue = moduleFn._invokeQueue, i = 0, ii = invokeQueue.length; i < ii; i++) { - var invokeArgs = invokeQueue[i], - provider = providerInjector.get(invokeArgs[0]); - - provider[invokeArgs[1]].apply(provider, invokeArgs[2]); - } + runInvokeQueue(moduleFn._invokeQueue); + runInvokeQueue(moduleFn._configBlocks); } else if (isFunction(module)) { runBlocks.push(providerInjector.invoke(module)); } else if (isArray(module)) { @@ -3891,9 +4068,14 @@ function createInjector(modulesToLoad) { } } - function invoke(fn, self, locals){ + function invoke(fn, self, locals, serviceName) { + if (typeof locals === 'string') { + serviceName = locals; + locals = null; + } + var args = [], - $inject = annotate(fn), + $inject = annotate(fn, strictDi, serviceName), length, i, key; @@ -3918,7 +4100,7 @@ function createInjector(modulesToLoad) { return fn.apply(self, args); } - function instantiate(Type, locals) { + function instantiate(Type, locals, serviceName) { var Constructor = function() {}, instance, returnedValue; @@ -3926,7 +4108,7 @@ function createInjector(modulesToLoad) { // e.g. someModule.factory('greeter', ['$window', function(renamed$window) {}]); Constructor.prototype = (isArray(Type) ? Type[Type.length - 1] : Type).prototype; instance = new Constructor(); - returnedValue = invoke(Type, instance, locals); + returnedValue = invoke(Type, instance, locals, serviceName); return isObject(returnedValue) || isFunction(returnedValue) ? returnedValue : instance; } @@ -3943,6 +4125,8 @@ function createInjector(modulesToLoad) { } } +createInjector.$$annotate = annotate; + /** * @ngdoc service * @name $anchorScroll @@ -3960,24 +4144,26 @@ function createInjector(modulesToLoad) { * This can be disabled by calling `$anchorScrollProvider.disableAutoScrolling()`. * * @example - <example> + <example module="anchorScrollExample"> <file name="index.html"> - <div id="scrollArea" ng-controller="ScrollCtrl"> + <div id="scrollArea" ng-controller="ScrollController"> <a ng-click="gotoBottom()">Go to bottom</a> <a id="bottom"></a> You're at the bottom! </div> </file> <file name="script.js"> - function ScrollCtrl($scope, $location, $anchorScroll) { - $scope.gotoBottom = function (){ - // set the location.hash to the id of - // the element you wish to scroll to. - $location.hash('bottom'); - - // call $anchorScroll() - $anchorScroll(); - }; - } + angular.module('anchorScrollExample', []) + .controller('ScrollController', ['$scope', '$location', '$anchorScroll', + function ($scope, $location, $anchorScroll) { + $scope.gotoBottom = function() { + // set the location.hash to the id of + // the element you wish to scroll to. + $location.hash('bottom'); + + // call $anchorScroll() + $anchorScroll(); + }; + }]); </file> <file name="style.css"> #scrollArea { @@ -4010,7 +4196,7 @@ function $AnchorScrollProvider() { function getFirstAnchor(list) { var result = null; forEach(list, function(element) { - if (!result && lowercase(element.nodeName) === 'a') result = element; + if (!result && nodeName_(element) === 'a') result = element; }); return result; } @@ -4125,10 +4311,19 @@ var $AnimateProvider = ['$provide', function($provide) { return this.$$classNameFilter; }; - this.$get = ['$timeout', '$$asyncCallback', function($timeout, $$asyncCallback) { + this.$get = ['$$q', '$$asyncCallback', function($$q, $$asyncCallback) { - function async(fn) { - fn && $$asyncCallback(fn); + var currentDefer; + function asyncPromise() { + // only serve one instance of a promise in order to save CPU cycles + if (!currentDefer) { + currentDefer = $$q.defer(); + $$asyncCallback(function() { + currentDefer.resolve(); + currentDefer = null; + }); + } + return currentDefer.promise; } /** @@ -4155,26 +4350,20 @@ var $AnimateProvider = ['$provide', function($provide) { * @ngdoc method * @name $animate#enter * @kind function - * @description Inserts the element into the DOM either after the `after` element or within - * the `parent` element. Once complete, the done() callback will be fired (if provided). + * @description Inserts the element into the DOM either after the `after` element or + * as the first child within the `parent` element. When the function is called a promise + * is returned that will be resolved at a later time. * @param {DOMElement} element the element which will be inserted into the DOM * @param {DOMElement} parent the parent element which will append the element as * a child (if the after element is not present) * @param {DOMElement} after the sibling element which will append the element * after itself - * @param {Function=} done callback function that will be called after the element has been - * inserted into the DOM + * @return {Promise} the animation callback promise */ - enter : function(element, parent, after, done) { - if (after) { - after.after(element); - } else { - if (!parent || !parent[0]) { - parent = after.parent(); - } - parent.append(element); - } - async(done); + enter : function(element, parent, after) { + after ? after.after(element) + : parent.prepend(element); + return asyncPromise(); }, /** @@ -4182,15 +4371,14 @@ var $AnimateProvider = ['$provide', function($provide) { * @ngdoc method * @name $animate#leave * @kind function - * @description Removes the element from the DOM. Once complete, the done() callback will be - * fired (if provided). + * @description Removes the element from the DOM. When the function is called a promise + * is returned that will be resolved at a later time. * @param {DOMElement} element the element which will be removed from the DOM - * @param {Function=} done callback function that will be called after the element has been - * removed from the DOM + * @return {Promise} the animation callback promise */ - leave : function(element, done) { + leave : function(element) { element.remove(); - async(done); + return asyncPromise(); }, /** @@ -4199,8 +4387,8 @@ var $AnimateProvider = ['$provide', function($provide) { * @name $animate#move * @kind function * @description Moves the position of the provided element within the DOM to be placed - * either after the `after` element or inside of the `parent` element. Once complete, the - * done() callback will be fired (if provided). + * either after the `after` element or inside of the `parent` element. When the function + * is called a promise is returned that will be resolved at a later time. * * @param {DOMElement} element the element which will be moved around within the * DOM @@ -4208,13 +4396,12 @@ var $AnimateProvider = ['$provide', function($provide) { * inserted into (if the after element is not present) * @param {DOMElement} after the sibling element where the element will be * positioned next to - * @param {Function=} done the callback function (if provided) that will be fired after the - * element has been moved to its new position + * @return {Promise} the animation callback promise */ - move : function(element, parent, after, done) { + move : function(element, parent, after) { // Do not remove element before insert. Removing will cause data associated with the // element to be dropped. Insert will implicitly do the remove. - this.enter(element, parent, after, done); + return this.enter(element, parent, after); }, /** @@ -4222,22 +4409,21 @@ var $AnimateProvider = ['$provide', function($provide) { * @ngdoc method * @name $animate#addClass * @kind function - * @description Adds the provided className CSS class value to the provided element. Once - * complete, the done() callback will be fired (if provided). + * @description Adds the provided className CSS class value to the provided element. + * When the function is called a promise is returned that will be resolved at a later time. * @param {DOMElement} element the element which will have the className value * added to it * @param {string} className the CSS class which will be added to the element - * @param {Function=} done the callback function (if provided) that will be fired after the - * className value has been added to the element + * @return {Promise} the animation callback promise */ - addClass : function(element, className, done) { - className = isString(className) ? - className : - isArray(className) ? className.join(' ') : ''; + addClass : function(element, className) { + className = !isString(className) + ? (isArray(className) ? className.join(' ') : '') + : className; forEach(element, function (element) { jqLiteAddClass(element, className); }); - async(done); + return asyncPromise(); }, /** @@ -4246,21 +4432,20 @@ var $AnimateProvider = ['$provide', function($provide) { * @name $animate#removeClass * @kind function * @description Removes the provided className CSS class value from the provided element. - * Once complete, the done() callback will be fired (if provided). + * When the function is called a promise is returned that will be resolved at a later time. * @param {DOMElement} element the element which will have the className value * removed from it * @param {string} className the CSS class which will be removed from the element - * @param {Function=} done the callback function (if provided) that will be fired after the - * className value has been removed from the element + * @return {Promise} the animation callback promise */ - removeClass : function(element, className, done) { - className = isString(className) ? - className : - isArray(className) ? className.join(' ') : ''; + removeClass : function(element, className) { + className = !isString(className) + ? (isArray(className) ? className.join(' ') : '') + : className; forEach(element, function (element) { jqLiteRemoveClass(element, className); }); - async(done); + return asyncPromise(); }, /** @@ -4269,23 +4454,21 @@ var $AnimateProvider = ['$provide', function($provide) { * @name $animate#setClass * @kind function * @description Adds and/or removes the given CSS classes to and from the element. - * Once complete, the done() callback will be fired (if provided). + * When the function is called a promise is returned that will be resolved at a later time. * @param {DOMElement} element the element which will have its CSS classes changed * removed from it * @param {string} add the CSS classes which will be added to the element * @param {string} remove the CSS class which will be removed from the element - * @param {Function=} done the callback function (if provided) that will be fired after the - * CSS classes have been set on the element + * @return {Promise} the animation callback promise */ - setClass : function(element, add, remove, done) { - forEach(element, function (element) { - jqLiteAddClass(element, add); - jqLiteRemoveClass(element, remove); - }); - async(done); + setClass : function(element, add, remove) { + this.addClass(element, add); + this.removeClass(element, remove); + return asyncPromise(); }, - enabled : noop + enabled : noop, + cancel : noop }; }]; }]; @@ -4534,6 +4717,13 @@ function Browser(window, document, $log, $sniffer) { return callback; }; + /** + * Checks whether the url has changed outside of Angular. + * Needs to be exported to be able to check for changes that have been done in sync, + * as hashchange/popstate events fire in async. + */ + self.$$checkUrlChange = fireUrlChange; + ////////////////////////////////////////////////////////////// // Misc API ////////////////////////////////////////////////////////////// @@ -4580,16 +4770,15 @@ function Browser(window, document, $log, $sniffer) { * @returns {Object} Hash of all cookies (if called without any parameter) */ self.cookies = function(name, value) { - /* global escape: false, unescape: false */ var cookieLength, cookieArray, cookie, i, index; if (name) { if (value === undefined) { - rawDocument.cookie = escape(name) + "=;path=" + cookiePath + + rawDocument.cookie = encodeURIComponent(name) + "=;path=" + cookiePath + ";expires=Thu, 01 Jan 1970 00:00:00 GMT"; } else { if (isString(value)) { - cookieLength = (rawDocument.cookie = escape(name) + '=' + escape(value) + + cookieLength = (rawDocument.cookie = encodeURIComponent(name) + '=' + encodeURIComponent(value) + ';path=' + cookiePath).length + 1; // per http://www.ietf.org/rfc/rfc2109.txt browser must allow at minimum: @@ -4613,12 +4802,12 @@ function Browser(window, document, $log, $sniffer) { cookie = cookieArray[i]; index = cookie.indexOf('='); if (index > 0) { //ignore nameless cookies - name = unescape(cookie.substring(0, index)); + name = decodeURIComponent(cookie.substring(0, index)); // the first value that is seen for a cookie is the most // specific one. values for the same cookie name that // follow are for less specific paths. if (lastCookies[name] === undefined) { - lastCookies[name] = unescape(cookie.substring(index + 1)); + lastCookies[name] = decodeURIComponent(cookie.substring(index + 1)); } } } @@ -4750,8 +4939,10 @@ function $BrowserProvider(){ $scope.keys = []; $scope.cache = $cacheFactory('cacheId'); $scope.put = function(key, value) { - $scope.cache.put(key, value); - $scope.keys.push(key); + if ($scope.cache.get(key) === undefined) { + $scope.keys.push(key); + } + $scope.cache.put(key, value === undefined ? null : value); }; }]); </file> @@ -5191,6 +5382,13 @@ function $TemplateCacheProvider() { * The directive definition object provides instructions to the {@link ng.$compile * compiler}. The attributes are: * + * #### `multiElement` + * When this property is set to true, 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 recomended that this feature be used on directives + * which are not strictly behavioural (such as {@link api/ng.directive:ngClick ngClick}), and which + * do not manipulate or replace child nodes (such as {@link api/ng.directive:ngInclude ngInclude}). + * * #### `priority` * When there are multiple directives defined on a single DOM element, sometimes it * is necessary to specify the order in which the directives are applied. The `priority` is used @@ -5248,6 +5446,10 @@ function $TemplateCacheProvider() { * by calling the `localFn` as `localFn({amount: 22})`. * * + * #### `bindToController` + * When an isolate scope is used for a component (see above), and `controllerAs` is used, `bindToController` will + * allow a component to have its properties bound to the controller, rather than to scope. When the controller + * is instantiated, the initial values of the isolate scope bindings are already available. * * #### `controller` * Controller constructor function. The controller is instantiated before the @@ -5258,9 +5460,18 @@ function $TemplateCacheProvider() { * * `$scope` - Current scope associated with the element * * `$element` - Current element * * `$attrs` - Current attributes object for the element - * * `$transclude` - A transclude linking function pre-bound to the correct transclusion scope. - * The scope can be overridden by an optional first argument. - * `function([scope], cloneLinkingFn)`. + * * `$transclude` - A transclude linking function pre-bound to the correct transclusion scope: + * `function([scope], cloneLinkingFn, futureParentElement)`. + * * `scope`: optional argument to override the scope. + * * `cloneLinkingFn`: optional argument to create clones of the original transcluded content. + * * `futureParentElement`: + * * 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, + * 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. * * * #### `require` @@ -5271,9 +5482,9 @@ function $TemplateCacheProvider() { * * * (no prefix) - Locate the required controller on the current element. Throw an error if not found. * * `?` - Attempt to locate the required controller or pass `null` to the `link` fn if not found. - * * `^` - Locate the required controller by searching the element's parents. Throw an error if not found. - * * `?^` - Attempt to locate the required controller by searching the element's parents or pass `null` to the - * `link` fn if not found. + * * `^` - Locate the required controller by searching the element and its parents. Throw an error if not found. + * * `?^` - Attempt to locate the required controller by searching the element and its parents or pass + * `null` to the `link` fn if not found. * * * #### `controllerAs` @@ -5284,29 +5495,51 @@ function $TemplateCacheProvider() { * * #### `restrict` * String of subset of `EACM` which restricts the directive to a specific directive - * declaration style. If omitted, the default (attributes only) is used. + * declaration style. If omitted, the defaults (elements and attributes) are used. * - * * `E` - Element name: `<my-directive></my-directive>` + * * `E` - Element name (default): `<my-directive></my-directive>` * * `A` - Attribute (default): `<div my-directive="exp"></div>` * * `C` - Class: `<div class="my-directive: exp;"></div>` * * `M` - Comment: `<!-- directive: my-directive exp -->` * * + * #### `templateNamespace` + * String representing the document type used by the markup in the template. + * AngularJS needs this information as those elements need to be created and cloned + * in a special way when they are defined outside their usual containers like `<svg>` and `<math>`. + * + * * `html` - All root nodes in the template are HTML. Root nodes may also be + * top-level elements such as `<svg>` or `<math>`. + * * `svg` - The root nodes in the template are SVG elements (excluding `<math>`). + * * `math` - The root nodes in the template are MathML elements (excluding `<svg>`). + * + * If no `templateNamespace` is specified, then the namespace is considered to be `html`. + * * #### `template` - * replace the current element with the contents of the HTML. The replacement process - * migrates all of the attributes / classes from the old element to the new one. See the - * {@link guide/directive#creating-custom-directives_creating-directives_template-expanding-directive - * Directives Guide} for an example. + * HTML markup that may: + * * Replace the contents of the directive's element (default). + * * Replace the directive's element itself (if `replace` is true - DEPRECATED). + * * Wrap the contents of the directive's element (if `transclude` is true). * - * You can specify `template` as a string representing the template or as a function which takes - * two arguments `tElement` and `tAttrs` (described in the `compile` function api below) and - * returns a string value representing the template. + * Value may be: + * + * * A string. For example `<div red-on-hover>{{delete_str}}</div>`. + * * A function which takes two arguments `tElement` and `tAttrs` (described in the `compile` + * function api below) and returns a string value. * * * #### `templateUrl` - * Same as `template` but the template is loaded from the specified URL. Because - * the template loading is asynchronous the compilation/linking is suspended until the template - * is loaded. + * This is similar to `template` but the template is loaded from the specified URL, asynchronously. + * + * Because template loading is asynchronous the compiler will suspend compilation of directives on that element + * for later when the template has been resolved. In the meantime it will continue to compile and link + * sibling and parent elements as though this element had not contained any directives. + * + * The compiler does not suspend the entire compilation to wait for templates to be loaded because this + * would result in the whole app "stalling" until all templates are loaded asynchronously - even in the + * case when only one deeply nested directive has `templateUrl`. + * + * Template loading is asynchronous even if the template has been preloaded into the {@link $templateCache} * * You can specify `templateUrl` as a string representing the URL or as a function which takes two * arguments `tElement` and `tAttrs` (described in the `compile` function api below) and returns @@ -5314,12 +5547,19 @@ function $TemplateCacheProvider() { * api/ng.$sce#getTrustedResourceUrl $sce.getTrustedResourceUrl}. * * - * #### `replace` ([*DEPRECATED*!], will be removed in next major release) - * specify where the template should be inserted. Defaults to `false`. + * #### `replace` ([*DEPRECATED*!], will be removed in next major release - i.e. v2.0) + * specify what the template should replace. Defaults to `false`. * - * * `true` - the template will replace the current element. - * * `false` - the template will replace the contents of the current element. + * * `true` - the template will replace the directive's element. + * * `false` - the template will replace the contents of the directive's element. * + * The replacement process migrates all of the attributes / classes from the old element to the new + * one. See the {@link guide/directive#creating-custom-directives_creating-directives_template-expanding-directive + * Directives Guide} for an example. + * + * There are very few scenarios where element replacement is required for the application function, + * the main one being reusable custom components that are used within SVG contexts + * (because SVG doesn't work with custom elements in the DOM tree). * * #### `transclude` * compile the content of the element and make it available to the directive. @@ -5333,6 +5573,11 @@ function $TemplateCacheProvider() { * * `true` - transclude the content of the directive. * * `'element'` - transclude the whole element including any directives defined at lower priority. * + * <div class="alert alert-warning"> + * **Note:** When testing an element transclude directive you must not place the directive at the root of the + * DOM fragment that is being compiled. See {@link guide/unit-testing#testing-transclusion-directives + * Testing Transclusion Directives}. + * </div> * * #### `compile` * @@ -5410,10 +5655,9 @@ function $TemplateCacheProvider() { * the directives to use the controllers as a communication channel. * * * `transcludeFn` - A transclude linking function pre-bound to the correct transclusion scope. - * The scope can be overridden by an optional first argument. This is the same as the `$transclude` - * parameter of directive controllers. - * `function([scope], cloneLinkingFn)`. - * + * This is the same as the `$transclude` + * parameter of directive controllers, see there for details. + * `function([scope], cloneLinkingFn, futureParentElement)`. * * #### Pre-linking function * @@ -5422,7 +5666,14 @@ function $TemplateCacheProvider() { * * #### Post-linking function * - * Executed after the child elements are linked. It is safe to do DOM transformation in the post-linking function. + * Executed after the child elements are linked. + * + * Note that child elements that contain `templateUrl` directives will not have been compiled + * and linked since they are waiting for their template to load asynchronously and their own + * compilation and linking has been suspended until that occurs. + * + * It is safe to do DOM transformation in the post-linking function on elements that are not waiting + * for their async templates to be resolved. * * <a name="Attributes"></a> * ### Attributes @@ -5578,7 +5829,6 @@ var $compileMinErr = minErr('$compile'); /** * @ngdoc provider * @name $compileProvider - * @kind function * * @description */ @@ -5587,7 +5837,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { var hasDirectives = {}, Suffix = 'Directive', COMMENT_DIRECTIVE_REGEXP = /^\s*directive\:\s*([\d\w_\-]+)\s+(.*)$/, - CLASS_DIRECTIVE_REGEXP = /(([\d\w_\-]+)(?:\:([^;]+))?;?)/; + CLASS_DIRECTIVE_REGEXP = /(([\d\w_\-]+)(?:\:([^;]+))?;?)/, + ALL_OR_NOTHING_ATTRS = makeMap('ngSrc,ngSrcset,src,srcset'); // Ref: http://developers.whatwg.org/webappapis.html#event-handler-idl-attributes // The assumption is that future DOM event attribute names will begin with @@ -5630,7 +5881,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { directive.index = index; directive.name = directive.name || name; directive.require = directive.require || (directive.controller && directive.name); - directive.restrict = directive.restrict || 'A'; + directive.restrict = directive.restrict || 'EA'; directives.push(directive); } catch (e) { $exceptionHandler(e); @@ -5706,15 +5957,57 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } }; + /** + * @ngdoc method + * @name $compileProvider#debugInfoEnabled + * + * @param {boolean=} enabled update the debugInfoEnabled state if provided, otherwise just return the + * current debugInfoEnabled state + * @returns {*} current value if used as getter or itself (chaining) if used as setter + * + * @kind function + * + * @description + * Call this method to enable/disable various debug runtime information in the compiler such as adding + * binding information and a reference to the current scope on to DOM elements. + * If enabled, the compiler will add the following to DOM elements that have been bound to the scope + * * `ng-binding` CSS class + * * `$binding` data property containing an array of the binding expressions + * + * You may want to use this in production for a significant performance boost. See + * {@link guide/production#disabling-debug-data Disabling Debug Data} for more. + * + * The default value is true. + */ + var debugInfoEnabled = true; + this.debugInfoEnabled = function(enabled) { + if(isDefined(enabled)) { + debugInfoEnabled = enabled; + return this; + } + return debugInfoEnabled; + }; + this.$get = [ - '$injector', '$interpolate', '$exceptionHandler', '$http', '$templateCache', '$parse', + '$injector', '$interpolate', '$exceptionHandler', '$templateRequest', '$parse', '$controller', '$rootScope', '$document', '$sce', '$animate', '$$sanitizeUri', - function($injector, $interpolate, $exceptionHandler, $http, $templateCache, $parse, + function($injector, $interpolate, $exceptionHandler, $templateRequest, $parse, $controller, $rootScope, $document, $sce, $animate, $$sanitizeUri) { - var Attributes = function(element, attr) { + var Attributes = function(element, attributesToCopy) { + if (attributesToCopy) { + var keys = Object.keys(attributesToCopy); + var i, l, key; + + for (i = 0, l = keys.length; i < l; i++) { + key = keys[i]; + this[key] = attributesToCopy[key]; + } + } else { + this.$attr = {}; + } + this.$$element = element; - this.$attr = attr || {}; }; Attributes.prototype = { @@ -5769,14 +6062,13 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { */ $updateClass : function(newClasses, oldClasses) { var toAdd = tokenDifference(newClasses, oldClasses); - var toRemove = tokenDifference(oldClasses, newClasses); + if (toAdd && toAdd.length) { + $animate.addClass(this.$$element, toAdd); + } - if(toAdd.length === 0) { + var toRemove = tokenDifference(oldClasses, newClasses); + if (toRemove && toRemove.length) { $animate.removeClass(this.$$element, toRemove); - } else if(toRemove.length === 0) { - $animate.addClass(this.$$element, toAdd); - } else { - $animate.setClass(this.$$element, toAdd, toRemove); } }, @@ -5794,13 +6086,19 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { //is set through this function since it may cause $updateClass to //become unstable. - var booleanKey = getBooleanAttrName(this.$$element[0], key), + var node = this.$$element[0], + booleanKey = getBooleanAttrName(node, key), + aliasedKey = getAliasedAttrName(node, key), + observer = key, normalizedVal, nodeName; if (booleanKey) { this.$$element.prop(key, value); attrName = booleanKey; + } else if(aliasedKey) { + this[aliasedKey] = value; + observer = aliasedKey; } this[key] = value; @@ -5818,8 +6116,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { nodeName = nodeName_(this.$$element); // sanitize a[href] and img[src] values - if ((nodeName === 'A' && key === 'href') || - (nodeName === 'IMG' && key === 'src')) { + if ((nodeName === 'a' && key === 'href') || + (nodeName === 'img' && key === 'src')) { this[key] = value = $$sanitizeUri(value, key === 'src'); } @@ -5833,7 +6131,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { // fire observers var $$observers = this.$$observers; - $$observers && forEach($$observers[key], function(fn) { + $$observers && forEach($$observers[observer], function(fn) { try { fn(value); } catch (e) { @@ -5859,7 +6157,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { * @param {function(interpolatedValue)} fn Function that will be called whenever the interpolated value of the attribute changes. * See the {@link guide/directive#Attributes Directives} guide for more info. - * @returns {function()} the `fn` parameter. + * @returns {function()} Returns a deregistration function for this observer. */ $observe: function(key, fn) { var attrs = this, @@ -5873,10 +6171,24 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { fn(attrs[key]); } }); - return fn; + + return function() { + arrayRemove(listeners, fn); + }; } }; + + function safeAddClass($element, className) { + try { + $element.addClass(className); + } catch(e) { + // ignore, since it means that we are trying to set class on + // SVG element, where class name is read-only. + } + } + + var startSymbol = $interpolate.startSymbol(), endSymbol = $interpolate.endSymbol(), denormalizeTemplate = (startSymbol == '{{' || endSymbol == '}}') @@ -5886,6 +6198,30 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { }, NG_ATTR_BINDING = /^ngAttr[A-Z]/; + compile.$$addBindingInfo = debugInfoEnabled ? function $$addBindingInfo($element, binding) { + var bindings = $element.data('$binding') || []; + + if (isArray(binding)) { + bindings = bindings.concat(binding); + } else { + bindings.push(binding); + } + + $element.data('$binding', bindings); + } : noop; + + compile.$$addBindingClass = debugInfoEnabled ? function $$addBindingClass($element) { + safeAddClass($element, 'ng-binding'); + } : noop; + + compile.$$addScopeInfo = debugInfoEnabled ? function $$addScopeInfo($element, scope, isolated, noTemplate) { + var dataName = isolated ? (noTemplate ? '$isolateScopeNoTemplate' : '$isolateScope') : '$scope'; + $element.data(dataName, scope); + } : noop; + + compile.$$addScopeClass = debugInfoEnabled ? function $$addScopeClass($element, isolated) { + safeAddClass($element, isolated ? 'ng-isolate-scope' : 'ng-scope'); + } : noop; return compile; @@ -5902,46 +6238,57 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { // not be able to attach scope data to them, so we will wrap them in <span> forEach($compileNodes, function(node, index){ if (node.nodeType == 3 /* text node */ && node.nodeValue.match(/\S+/) /* non-empty */ ) { - $compileNodes[index] = node = jqLite(node).wrap('<span></span>').parent()[0]; + $compileNodes[index] = jqLite(node).wrap('<span></span>').parent()[0]; } }); var compositeLinkFn = compileNodes($compileNodes, transcludeFn, $compileNodes, maxPriority, ignoreDirective, previousCompileContext); - safeAddClass($compileNodes, 'ng-scope'); - return function publicLinkFn(scope, cloneConnectFn, transcludeControllers, parentBoundTranscludeFn){ + compile.$$addScopeClass($compileNodes); + var namespace = null; + var namespaceAdaptedCompileNodes = $compileNodes; + var lastCompileNode; + return function publicLinkFn(scope, cloneConnectFn, transcludeControllers, parentBoundTranscludeFn, futureParentElement){ assertArg(scope, 'scope'); + if (!namespace) { + namespace = detectNamespaceForChildElements(futureParentElement); + } + if (namespace !== 'html' && $compileNodes[0] !== lastCompileNode) { + namespaceAdaptedCompileNodes = jqLite( + wrapTemplate(namespace, jqLite('<div>').append($compileNodes).html()) + ); + } + // When using a directive with replace:true and templateUrl the $compileNodes + // might change, so we need to recreate the namespace adapted compileNodes. + lastCompileNode = $compileNodes[0]; + // important!!: we must call our jqLite.clone() since the jQuery one is trying to be smart // and sometimes changes the structure of the DOM. var $linkNode = cloneConnectFn - ? JQLitePrototype.clone.call($compileNodes) // IMPORTANT!!! - : $compileNodes; + ? JQLitePrototype.clone.call(namespaceAdaptedCompileNodes) // IMPORTANT!!! + : namespaceAdaptedCompileNodes; - forEach(transcludeControllers, function(instance, name) { - $linkNode.data('$' + name + 'Controller', instance); - }); - - // Attach scope only to non-text nodes. - for(var i = 0, ii = $linkNode.length; i<ii; i++) { - var node = $linkNode[i], - nodeType = node.nodeType; - if (nodeType === 1 /* element */ || nodeType === 9 /* document */) { - $linkNode.eq(i).data('$scope', scope); + if (transcludeControllers) { + for (var controllerName in transcludeControllers) { + $linkNode.data('$' + controllerName + 'Controller', transcludeControllers[controllerName].instance); } } + compile.$$addScopeInfo($linkNode, scope); + if (cloneConnectFn) cloneConnectFn($linkNode, scope); if (compositeLinkFn) compositeLinkFn(scope, $linkNode, $linkNode, parentBoundTranscludeFn); return $linkNode; }; } - function safeAddClass($element, className) { - try { - $element.addClass(className); - } catch(e) { - // ignore, since it means that we are trying to set class on - // SVG element, where class name is read-only. + function detectNamespaceForChildElements(parentElement) { + // TODO: Make this detect MathML as well... + var node = parentElement && parentElement[0]; + if (!node) { + return 'html'; + } else { + return nodeName_(node) !== 'foreignobject' && node.toString().match(/SVG/) ? 'svg': 'html'; } } @@ -5963,7 +6310,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { function compileNodes(nodeList, transcludeFn, $rootElement, maxPriority, ignoreDirective, previousCompileContext) { var linkFns = [], - attrs, directives, nodeLinkFn, childNodes, childLinkFn, linkFnFound; + attrs, directives, nodeLinkFn, childNodes, childLinkFn, linkFnFound, nodeLinkFnFound; for (var i = 0; i < nodeList.length; i++) { attrs = new Attributes(); @@ -5978,7 +6325,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { : null; if (nodeLinkFn && nodeLinkFn.scope) { - safeAddClass(jqLite(nodeList[i]), 'ng-scope'); + compile.$$addScopeClass(attrs.$$element); } childLinkFn = (nodeLinkFn && nodeLinkFn.terminal || @@ -5990,8 +6337,12 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { (nodeLinkFn.transcludeOnThisElement || !nodeLinkFn.templateOnThisElement) && nodeLinkFn.transclude) : transcludeFn); - linkFns.push(nodeLinkFn, childLinkFn); - linkFnFound = linkFnFound || nodeLinkFn || childLinkFn; + if (nodeLinkFn || childLinkFn) { + linkFns.push(i, nodeLinkFn, childLinkFn); + linkFnFound = true; + nodeLinkFnFound = nodeLinkFnFound || nodeLinkFn; + } + //use the previous context only for the first element in the virtual group previousCompileContext = null; } @@ -6000,31 +6351,42 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { return linkFnFound ? compositeLinkFn : null; function compositeLinkFn(scope, nodeList, $rootElement, parentBoundTranscludeFn) { - var nodeLinkFn, childLinkFn, node, $node, childScope, i, ii, n, childBoundTranscludeFn; + var nodeLinkFn, childLinkFn, node, childScope, i, ii, idx, childBoundTranscludeFn; + var stableNodeList; - // copy nodeList so that linking doesn't break due to live list updates. - var nodeListLength = nodeList.length, - stableNodeList = new Array(nodeListLength); - for (i = 0; i < nodeListLength; i++) { - stableNodeList[i] = nodeList[i]; + + if (nodeLinkFnFound) { + // copy nodeList so that if a nodeLinkFn removes or adds an element at this DOM level our + // offsets don't get screwed up + var nodeListLength = nodeList.length; + stableNodeList = new Array(nodeListLength); + + // create a sparse array by only copying the elements which have a linkFn + for (i = 0; i < linkFns.length; i+=3) { + idx = linkFns[i]; + stableNodeList[idx] = nodeList[idx]; + } + } else { + stableNodeList = nodeList; } - for(i = 0, n = 0, ii = linkFns.length; i < ii; n++) { - node = stableNodeList[n]; + for(i = 0, ii = linkFns.length; i < ii;) { + node = stableNodeList[linkFns[i++]]; nodeLinkFn = linkFns[i++]; childLinkFn = linkFns[i++]; - $node = jqLite(node); if (nodeLinkFn) { if (nodeLinkFn.scope) { childScope = scope.$new(); - $node.data('$scope', childScope); + compile.$$addScopeInfo(jqLite(node), childScope); } else { childScope = scope; } if ( nodeLinkFn.transcludeOnThisElement ) { - childBoundTranscludeFn = createBoundTranscludeFn(scope, nodeLinkFn.transclude, parentBoundTranscludeFn); + childBoundTranscludeFn = createBoundTranscludeFn( + scope, nodeLinkFn.transclude, parentBoundTranscludeFn, + nodeLinkFn.elementTranscludeOnThisElement); } else if (!nodeLinkFn.templateOnThisElement && parentBoundTranscludeFn) { childBoundTranscludeFn = parentBoundTranscludeFn; @@ -6045,9 +6407,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } } - function createBoundTranscludeFn(scope, transcludeFn, previousBoundTranscludeFn) { + function createBoundTranscludeFn(scope, transcludeFn, previousBoundTranscludeFn, elementTransclusion) { - var boundTranscludeFn = function(transcludedScope, cloneFn, controllers) { + var boundTranscludeFn = function(transcludedScope, cloneFn, controllers, futureParentElement) { var scopeCreated = false; if (!transcludedScope) { @@ -6056,8 +6418,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { scopeCreated = true; } - var clone = transcludeFn(transcludedScope, cloneFn, controllers, previousBoundTranscludeFn); - if (scopeCreated) { + var clone = transcludeFn(transcludedScope, cloneFn, controllers, previousBoundTranscludeFn, futureParentElement); + if (scopeCreated && !elementTransclusion) { clone.on('$destroy', function() { transcludedScope.$destroy(); }); } return clone; @@ -6086,7 +6448,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { case 1: /* Element */ // use the node name: <directive> addDirective(directives, - directiveNormalize(nodeName_(node).toLowerCase()), 'E', maxPriority, ignoreDirective); + directiveNormalize(nodeName_(node)), 'E', maxPriority, ignoreDirective); // iterate over the attributes for (var attr, name, nName, ngAttrName, value, isNgAttr, nAttrs = node.attributes, @@ -6106,10 +6468,12 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } var directiveNName = ngAttrName.replace(/(Start|End)$/, ''); - if (ngAttrName === directiveNName + 'Start') { - attrStartName = name; - attrEndName = name.substr(0, name.length - 5) + 'end'; - name = name.substr(0, name.length - 6); + if (directiveIsMultiElement(directiveNName)) { + if (ngAttrName === directiveNName + 'Start') { + attrStartName = name; + attrEndName = name.substr(0, name.length - 5) + 'end'; + name = name.substr(0, name.length - 6); + } } nName = directiveNormalize(name.toLowerCase()); @@ -6120,7 +6484,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { attrs[nName] = true; // presence means true } } - addAttrInterpolateDirective(node, directives, value, nName); + addAttrInterpolateDirective(node, directives, value, nName, isNgAttr); addDirective(directives, nName, 'A', maxPriority, ignoreDirective, attrStartName, attrEndName); } @@ -6241,6 +6605,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { var terminalPriority = -Number.MAX_VALUE, newScopeDirective, controllerDirectives = previousCompileContext.controllerDirectives, + controllers, newIsolateScopeDirective = previousCompileContext.newIsolateScopeDirective, templateDirective = previousCompileContext.templateDirective, nonTlbTranscludeDirective = previousCompileContext.nonTlbTranscludeDirective, @@ -6273,17 +6638,25 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } if (directiveValue = directive.scope) { - newScopeDirective = newScopeDirective || directive; // skip the check for directives with async templates, we'll check the derived sync // directive when the template arrives if (!directive.templateUrl) { - assertNoDuplicate('new/isolated scope', newIsolateScopeDirective, directive, - $compileNode); if (isObject(directiveValue)) { + // This directive is trying to add an isolated scope. + // Check that there is no scope of any kind already + assertNoDuplicate('new/isolated scope', newIsolateScopeDirective || newScopeDirective, + directive, $compileNode); newIsolateScopeDirective = directive; + } else { + // This directive is trying to add a child scope. + // Check that there is no isolated scope already + assertNoDuplicate('new/isolated scope', newIsolateScopeDirective, directive, + $compileNode); } } + + newScopeDirective = newScopeDirective || directive; } directiveName = directive.name; @@ -6310,12 +6683,12 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (directiveValue == 'element') { hasElementTranscludeDirective = true; terminalPriority = directive.priority; - $template = groupScan(compileNode, attrStart, attrEnd); + $template = $compileNode; $compileNode = templateAttrs.$$element = jqLite(document.createComment(' ' + directiveName + ': ' + templateAttrs[directiveName] + ' ')); compileNode = $compileNode[0]; - replaceWith(jqCollection, jqLite(sliceArgs($template)), compileNode); + replaceWith(jqCollection, sliceArgs($template), compileNode); childTranscludeFn = compile($template, transcludeFn, terminalPriority, replaceDirective && replaceDirective.name, { @@ -6351,7 +6724,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (jqLiteIsTextNode(directiveValue)) { $template = []; } else { - $template = jqLite(trim(directiveValue)); + $template = jqLite(wrapTemplate(directive.templateNamespace, trim(directiveValue))); } compileNode = $template[0]; @@ -6424,6 +6797,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { nodeLinkFn.scope = newScopeDirective && newScopeDirective.scope === true; nodeLinkFn.transcludeOnThisElement = hasTranscludeDirective; + nodeLinkFn.elementTranscludeOnThisElement = hasElementTranscludeDirective; nodeLinkFn.templateOnThisElement = hasTemplate; nodeLinkFn.transclude = childTranscludeFn; @@ -6469,7 +6843,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { value = null; if (elementControllers && retrievalMethod === 'data') { - value = elementControllers[require]; + if (value = elementControllers[require]) { + value = value.instance; + } } value = value || $element[retrievalMethod]('$' + require + 'Controller'); @@ -6490,32 +6866,68 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { function nodeLinkFn(childLinkFn, scope, linkNode, $rootElement, boundTranscludeFn) { - var attrs, $element, i, ii, linkFn, controller, isolateScope, elementControllers = {}, transcludeFn; + var i, ii, linkFn, controller, isolateScope, elementControllers, transcludeFn, $element, + attrs; if (compileNode === linkNode) { attrs = templateAttrs; + $element = templateAttrs.$$element; } else { - attrs = shallowCopy(templateAttrs, new Attributes(jqLite(linkNode), templateAttrs.$attr)); + $element = jqLite(linkNode); + attrs = new Attributes($element, templateAttrs); } - $element = attrs.$$element; if (newIsolateScopeDirective) { - var LOCAL_REGEXP = /^\s*([@=&])(\??)\s*(\w*)\s*$/; - var $linkNode = jqLite(linkNode); - isolateScope = scope.$new(true); + } - if (templateDirective && (templateDirective === newIsolateScopeDirective || - templateDirective === newIsolateScopeDirective.$$originalDirective)) { - $linkNode.data('$isolateScope', isolateScope) ; - } else { - $linkNode.data('$isolateScopeNoTemplate', isolateScope); - } + transcludeFn = boundTranscludeFn && controllersBoundTransclude; + if (controllerDirectives) { + // TODO: merge `controllers` and `elementControllers` into single object. + controllers = {}; + elementControllers = {}; + forEach(controllerDirectives, function(directive) { + var locals = { + $scope: directive === newIsolateScopeDirective || directive.$$isolateScope ? isolateScope : scope, + $element: $element, + $attrs: attrs, + $transclude: transcludeFn + }, controllerInstance; + + controller = directive.controller; + if (controller == '@') { + controller = attrs[directive.name]; + } + + controllerInstance = $controller(controller, locals, true, directive.controllerAs); + + // For directives with element transclusion the element is a comment, + // but jQuery .data doesn't support attaching data to comment nodes as it's hard to + // clean up (http://bugs.jquery.com/ticket/8335). + // Instead, we save the controllers for the element in a local hash and attach to .data + // later, once we have the actual element. + elementControllers[directive.name] = controllerInstance; + if (!hasElementTranscludeDirective) { + $element.data('$' + directive.name + 'Controller', controllerInstance.instance); + } + controllers[directive.name] = controllerInstance; + }); + } + if (newIsolateScopeDirective) { + var LOCAL_REGEXP = /^\s*([@=&])(\??)\s*(\w*)\s*$/; - safeAddClass($linkNode, 'ng-isolate-scope'); + compile.$$addScopeInfo($element, isolateScope, true, !(templateDirective && (templateDirective === newIsolateScopeDirective || + templateDirective === newIsolateScopeDirective.$$originalDirective))); + compile.$$addScopeClass($element, true); + var isolateScopeController = controllers && controllers[newIsolateScopeDirective.name]; + var isolateBindingContext = isolateScope; + if (isolateScopeController && isolateScopeController.identifier && + newIsolateScopeDirective.bindToController === true) { + isolateBindingContext = isolateScopeController.instance; + } forEach(newIsolateScopeDirective.scope, function(definition, scopeName) { var match = definition.match(LOCAL_REGEXP) || [], attrName = match[3] || scopeName, @@ -6536,7 +6948,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if( attrs[attrName] ) { // If the attribute has been provided then we trigger an interpolation to ensure // the value is there for use in the link fn - isolateScope[scopeName] = $interpolate(attrs[attrName])(scope); + isolateBindingContext[scopeName] = $interpolate(attrs[attrName])(scope); } break; @@ -6548,35 +6960,35 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (parentGet.literal) { compare = equals; } else { - compare = function(a,b) { return a === b; }; + compare = function(a,b) { return a === b || (a !== a && b !== b); }; } parentSet = parentGet.assign || function() { // reset the change, or we will throw this exception on every $digest - lastValue = isolateScope[scopeName] = parentGet(scope); + lastValue = isolateBindingContext[scopeName] = parentGet(scope); throw $compileMinErr('nonassign', "Expression '{0}' used with directive '{1}' is non-assignable!", attrs[attrName], newIsolateScopeDirective.name); }; - lastValue = isolateScope[scopeName] = parentGet(scope); - isolateScope.$watch(function parentValueWatch() { - var parentValue = parentGet(scope); - if (!compare(parentValue, isolateScope[scopeName])) { + lastValue = isolateBindingContext[scopeName] = parentGet(scope); + var unwatch = scope.$watch($parse(attrs[attrName], function parentValueWatch(parentValue) { + if (!compare(parentValue, isolateBindingContext[scopeName])) { // we are out of sync and need to copy if (!compare(parentValue, lastValue)) { // parent changed and it has precedence - isolateScope[scopeName] = parentValue; + isolateBindingContext[scopeName] = parentValue; } else { // if the parent can be assigned then do so - parentSet(scope, parentValue = isolateScope[scopeName]); + parentSet(scope, parentValue = isolateBindingContext[scopeName]); } } return lastValue = parentValue; - }, null, parentGet.literal); + }), null, parentGet.literal); + isolateScope.$on('$destroy', unwatch); break; case '&': parentGet = $parse(attrs[attrName]); - isolateScope[scopeName] = function(locals) { + isolateBindingContext[scopeName] = function(locals) { return parentGet(scope, locals); }; break; @@ -6589,47 +7001,23 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } }); } - transcludeFn = boundTranscludeFn && controllersBoundTransclude; - if (controllerDirectives) { - forEach(controllerDirectives, function(directive) { - var locals = { - $scope: directive === newIsolateScopeDirective || directive.$$isolateScope ? isolateScope : scope, - $element: $element, - $attrs: attrs, - $transclude: transcludeFn - }, controllerInstance; - - controller = directive.controller; - if (controller == '@') { - controller = attrs[directive.name]; - } - - controllerInstance = $controller(controller, locals); - // For directives with element transclusion the element is a comment, - // but jQuery .data doesn't support attaching data to comment nodes as it's hard to - // clean up (http://bugs.jquery.com/ticket/8335). - // Instead, we save the controllers for the element in a local hash and attach to .data - // later, once we have the actual element. - elementControllers[directive.name] = controllerInstance; - if (!hasElementTranscludeDirective) { - $element.data('$' + directive.name + 'Controller', controllerInstance); - } - - if (directive.controllerAs) { - locals.$scope[directive.controllerAs] = controllerInstance; - } + if (controllers) { + forEach(controllers, function(controller) { + controller(); }); + controllers = null; } // PRELINKING for(i = 0, ii = preLinkFns.length; i < ii; i++) { - try { - linkFn = preLinkFns[i]; - linkFn(linkFn.isolateScope ? isolateScope : scope, $element, attrs, - linkFn.require && getControllers(linkFn.directiveName, linkFn.require, $element, elementControllers), transcludeFn); - } catch (e) { - $exceptionHandler(e, startingTag($element)); - } + linkFn = preLinkFns[i]; + invokeLinkFn(linkFn, + linkFn.isolateScope ? isolateScope : scope, + $element, + attrs, + linkFn.require && getControllers(linkFn.directiveName, linkFn.require, $element, elementControllers), + transcludeFn + ); } // RECURSION @@ -6643,21 +7031,24 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { // POSTLINKING for(i = postLinkFns.length - 1; i >= 0; i--) { - try { - linkFn = postLinkFns[i]; - linkFn(linkFn.isolateScope ? isolateScope : scope, $element, attrs, - linkFn.require && getControllers(linkFn.directiveName, linkFn.require, $element, elementControllers), transcludeFn); - } catch (e) { - $exceptionHandler(e, startingTag($element)); - } + linkFn = postLinkFns[i]; + invokeLinkFn(linkFn, + linkFn.isolateScope ? isolateScope : scope, + $element, + attrs, + linkFn.require && getControllers(linkFn.directiveName, linkFn.require, $element, elementControllers), + transcludeFn + ); } // This is the function that is injected as `$transclude`. - function controllersBoundTransclude(scope, cloneAttachFn) { + // Note: all arguments are optional! + function controllersBoundTransclude(scope, cloneAttachFn, futureParentElement) { var transcludeControllers; - // no scope passed - if (arguments.length < 2) { + // No scope passed in: + if (!isScope(scope)) { + futureParentElement = cloneAttachFn; cloneAttachFn = scope; scope = undefined; } @@ -6665,8 +7056,10 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (hasElementTranscludeDirective) { transcludeControllers = elementControllers; } - - return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers); + if (!futureParentElement) { + futureParentElement = hasElementTranscludeDirective ? $element.parent() : $element; + } + return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement); } } } @@ -6717,6 +7110,27 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { /** + * looks up the directive and returns true if it is a multi-element directive, + * and therefore requires DOM nodes between -start and -end markers to be grouped + * together. + * + * @param {string} name name of the directive to look up. + * @returns true if directive was registered as multi-element. + */ + function directiveIsMultiElement(name) { + if (hasDirectives.hasOwnProperty(name)) { + for(var directive, directives = $injector.get(name + Suffix), + i = 0, ii = directives.length; i<ii; i++) { + directive = directives[i]; + if (directive.multiElement) { + return true; + } + } + } + return false; + } + + /** * When the element is replaced with HTML template then the new attributes * on the template need to be merged with the existing attributes in the DOM. * The desired effect is to have both of the attributes present. @@ -6771,12 +7185,13 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { }), templateUrl = (isFunction(origAsyncDirective.templateUrl)) ? origAsyncDirective.templateUrl($compileNode, tAttrs) - : origAsyncDirective.templateUrl; + : origAsyncDirective.templateUrl, + templateNamespace = origAsyncDirective.templateNamespace; $compileNode.empty(); - $http.get($sce.getTrustedResourceUrl(templateUrl), {cache: $templateCache}). - success(function(content) { + $templateRequest($sce.getTrustedResourceUrl(templateUrl)) + .then(function(content) { var compileNode, tempTemplateAttrs, $template, childBoundTranscludeFn; content = denormalizeTemplate(content); @@ -6785,7 +7200,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (jqLiteIsTextNode(content)) { $template = []; } else { - $template = jqLite(trim(content)); + $template = jqLite(wrapTemplate(templateNamespace, trim(content))); } compileNode = $template[0]; @@ -6836,7 +7251,6 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { // it was cloned therefore we have to clone as well. linkNode = jqLiteClone(compileNode); } - replaceWith(linkRootElement, jqLite(beforeTemplateLinkNode), linkNode); // Copy in CSS classes from original node @@ -6851,9 +7265,6 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { childBoundTranscludeFn); } linkQueue = null; - }). - error(function(response, code, headers, config) { - throw $compileMinErr('tpload', 'Failed to load template: {0}', config.url); }); return function delayedNodeLinkFn(ignoreChildLinkFn, scope, node, rootElement, boundTranscludeFn) { @@ -6892,31 +7303,45 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } - function addTextInterpolateDirective(directives, text) { - var interpolateFn = $interpolate(text, true); - if (interpolateFn) { - directives.push({ - priority: 0, - compile: function textInterpolateCompileFn(templateNode) { - // when transcluding a template that has bindings in the root - // then we don't have a parent and should do this in the linkFn - var parent = templateNode.parent(), hasCompileParent = parent.length; - if (hasCompileParent) safeAddClass(templateNode.parent(), 'ng-binding'); - - return function textInterpolateLinkFn(scope, node) { - var parent = node.parent(), - bindings = parent.data('$binding') || []; - bindings.push(interpolateFn); - parent.data('$binding', bindings); - if (!hasCompileParent) safeAddClass(parent, 'ng-binding'); - scope.$watch(interpolateFn, function interpolateFnWatchAction(value) { - node[0].nodeValue = value; - }); - }; - } - }); - } + function addTextInterpolateDirective(directives, text) { + var interpolateFn = $interpolate(text, true); + if (interpolateFn) { + directives.push({ + priority: 0, + compile: function textInterpolateCompileFn(templateNode) { + var templateNodeParent = templateNode.parent(), + hasCompileParent = !!templateNodeParent.length; + + // When transcluding a template that has bindings in the root + // we don't have a parent and thus need to add the class during linking fn. + if (hasCompileParent) compile.$$addBindingClass(templateNodeParent); + + return function textInterpolateLinkFn(scope, node) { + var parent = node.parent(); + if (!hasCompileParent) compile.$$addBindingClass(parent); + compile.$$addBindingInfo(parent, interpolateFn.expressions); + scope.$watch(interpolateFn, function interpolateFnWatchAction(value) { + node[0].nodeValue = value; + }); + }; + } + }); } + } + + + function wrapTemplate(type, template) { + type = lowercase(type || 'html'); + switch(type) { + case 'svg': + case 'math': + var wrapper = document.createElement('div'); + wrapper.innerHTML = '<'+type+'>'+template+'</'+type+'>'; + return wrapper.childNodes[0].childNodes; + default: + return template; + } + } function getTrustedContext(node, attrNormalizedName) { @@ -6926,22 +7351,22 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { var tag = nodeName_(node); // maction[xlink:href] can source SVG. It's not limited to <maction>. if (attrNormalizedName == "xlinkHref" || - (tag == "FORM" && attrNormalizedName == "action") || - (tag != "IMG" && (attrNormalizedName == "src" || + (tag == "form" && attrNormalizedName == "action") || + (tag != "img" && (attrNormalizedName == "src" || attrNormalizedName == "ngSrc"))) { return $sce.RESOURCE_URL; } } - function addAttrInterpolateDirective(node, directives, value, name) { + function addAttrInterpolateDirective(node, directives, value, name, allOrNothing) { var interpolateFn = $interpolate(value, true); // no interpolation found -> ignore if (!interpolateFn) return; - if (name === "multiple" && nodeName_(node) === "SELECT") { + if (name === "multiple" && nodeName_(node) === "select") { throw $compileMinErr("selmulti", "Binding to the 'multiple' attribute is not supported. Element: {0}", startingTag(node)); @@ -6962,15 +7387,18 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { // we need to interpolate again, in case the attribute value has been updated // (e.g. by another directive's compile function) - interpolateFn = $interpolate(attr[name], true, getTrustedContext(node, name)); + interpolateFn = $interpolate(attr[name], true, getTrustedContext(node, name), + ALL_OR_NOTHING_ATTRS[name] || allOrNothing); // if attribute was updated so that there is no interpolation going on we don't want to // register any observers if (!interpolateFn) return; - // TODO(i): this should likely be attr.$set(name, iterpolateFn(scope) so that we reset the - // actual attr value + // initialize attr object so that it's ready in case we need the value for isolate + // scope initialization, otherwise the value would not be available from isolate + // directive's linking fn during linking phase attr[name] = interpolateFn(scope); + ($$observers[name] || ($$observers[name] = [])).$$inter = true; (attr.$$observers && attr.$$observers[name].$$scope || scope). $watch(interpolateFn, function interpolateFnWatchAction(newValue, oldValue) { @@ -7023,6 +7451,13 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } } $rootElement.length -= removeCount - 1; + + // If the replaced element is also the jQuery .context then replace it + // .context is a deprecated jQuery api, so we should set it only when jQuery set it + // http://api.jquery.com/context/ + if ($rootElement.context === firstElementToRemove) { + $rootElement.context = newNode; + } break; } } @@ -7031,9 +7466,33 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (parent) { parent.replaceChild(newNode, firstElementToRemove); } + + // TODO(perf): what's this document fragment for? is it needed? can we at least reuse it? var fragment = document.createDocumentFragment(); fragment.appendChild(firstElementToRemove); - newNode[jqLite.expando] = firstElementToRemove[jqLite.expando]; + + // Copy over user data (that includes Angular's $scope etc.). Don't copy private + // data here because there's no public interface in jQuery to do that and copying over + // event listeners (which is the main use of private data) wouldn't work anyway. + jqLite(newNode).data(jqLite(firstElementToRemove).data()); + + // Remove data of the replaced element. We cannot just call .remove() + // on the element it since that would deallocate scope that is needed + // for the new node. Instead, remove the data "manually". + if (!jQuery) { + delete jqLite.cache[firstElementToRemove[jqLite.expando]]; + } else { + // jQuery 2.x doesn't expose the data storage. Use jQuery.cleanData to clean up after + // the replaced element. The cleanData version monkey-patched by Angular would cause + // the scope to be trashed and we do need the very same scope to work with the new + // element. However, we cannot just cache the non-patched version and use it here as + // that would break if another library patches the method after Angular does (one + // example is jQuery UI). Instead, set a flag indicating scope destroying should be + // skipped this one time. + skipDestroyOnNextJQueryCleanData = true; + jQuery.cleanData([firstElementToRemove]); + } + for (var k = 1, kk = elementsToRemove.length; k < kk; k++) { var element = elementsToRemove[k]; jqLite(element).remove(); // must do this way to clean up expando @@ -7049,6 +7508,15 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { function cloneAndAnnotateFn(fn, annotation) { return extend(function() { return fn.apply(null, arguments); }, fn, annotation); } + + + function invokeLinkFn(linkFn, scope, $element, attrs, controllers, transcludeFn) { + try { + linkFn(scope, $element, attrs, controllers, transcludeFn); + } catch(e) { + $exceptionHandler(e, startingTag($element)); + } + } }]; } @@ -7085,8 +7553,10 @@ function directiveNormalize(name) { /** * @ngdoc property * @name $compile.directive.Attributes#$attr - * @returns {object} A map of DOM element attribute names to the normalized name. This is - * needed to do reverse lookup from normalized name back to actual name. + * + * @description + * A map of DOM element attribute names to the normalized name. This is + * needed to do reverse lookup from normalized name back to actual name. */ @@ -7154,6 +7624,7 @@ function tokenDifference(str1, str2) { */ function $ControllerProvider() { var controllers = {}, + globals = false, CNTRL_REG = /^(\S+)(\s+as\s+(\w+))?$/; @@ -7174,6 +7645,15 @@ function $ControllerProvider() { } }; + /** + * @ngdoc method + * @name $controllerProvider#allowGlobals + * @description If called, allows `$controller` to find controller constructors on `window` + */ + this.allowGlobals = function() { + globals = true; + }; + this.$get = ['$injector', '$window', function($injector, $window) { @@ -7188,7 +7668,8 @@ 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 - * * check `window[constructor]` on the global `window` object + * * if $controllerProvider#allowGlobals, check `window[constructor]` on the global + * `window` object (not recommended) * * @param {Object} locals Injection locals for Controller. * @return {Object} Instance of given controller. @@ -7199,34 +7680,78 @@ function $ControllerProvider() { * It's just a simple call to {@link auto.$injector $injector}, but extracted into * a service, so that one can override this service with [BC version](https://gist.github.com/1649788). */ - return function(expression, locals) { + return function(expression, locals, later, ident) { + // PRIVATE API: + // param `later` --- indicates that the controller's constructor is invoked at a later time. + // If true, $controller will allocate the object with the correct + // prototype chain, but will not invoke the controller until a returned + // callback is invoked. + // param `ident` --- An optional label which overrides the label parsed from the controller + // expression, if any. var instance, match, constructor, identifier; + later = later === true; + if (ident && isString(ident)) { + identifier = ident; + } if(isString(expression)) { match = expression.match(CNTRL_REG), constructor = match[1], - identifier = match[3]; + identifier = identifier || match[3]; expression = controllers.hasOwnProperty(constructor) ? controllers[constructor] - : getter(locals.$scope, constructor, true) || getter($window, constructor, true); + : getter(locals.$scope, constructor, true) || + (globals ? getter($window, constructor, true) : undefined); assertArgFn(expression, constructor, true); } - instance = $injector.instantiate(expression, locals); - - if (identifier) { - if (!(locals && typeof locals.$scope === 'object')) { - throw minErr('$controller')('noscp', - "Cannot export controller '{0}' as '{1}'! No $scope object provided via `locals`.", - constructor || expression.name, identifier); + if (later) { + // Instantiate controller later: + // This machinery is used to create an instance of the object before calling the + // controller's constructor itself. + // + // This allows properties to be added to the controller before the constructor is + // invoked. Primarily, this is used for isolate scope bindings in $compile. + // + // This feature is not intended for use by applications, and is thus not documented + // publicly. + var Constructor = function() {}; + Constructor.prototype = (isArray(expression) ? + expression[expression.length - 1] : expression).prototype; + instance = new Constructor(); + + if (identifier) { + addIdentifier(locals, identifier, instance, constructor || expression.name); } - locals.$scope[identifier] = instance; + return extend(function() { + $injector.invoke(expression, instance, locals, constructor); + return instance; + }, { + instance: instance, + identifier: identifier + }); + } + + instance = $injector.instantiate(expression, locals, constructor); + + if (identifier) { + addIdentifier(locals, identifier, instance, constructor || expression.name); } return instance; }; + + function addIdentifier(locals, identifier, instance, name) { + if (!(locals && isObject(locals.$scope))) { + throw minErr('$controller')('noscp', + "Cannot export controller '{0}' as '{1}'! No $scope object provided via `locals`.", + name, identifier); + } + + locals.$scope[identifier] = instance; + } }]; } @@ -7318,11 +7843,7 @@ function parseHeaders(headers) { val = trim(line.substr(i + 1)); if (key) { - if (parsed[key]) { - parsed[key] += ', ' + val; - } else { - parsed[key] = val; - } + parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val; } }); @@ -7448,18 +7969,40 @@ function $HttpProvider() { xsrfHeaderName: 'X-XSRF-TOKEN' }; + var useApplyAsync = false; + /** + * @ngdoc method + * @name $httpProvider#useApplyAsync + * @description + * + * Configure $http service to combine processing of multiple http responses received at around + * the same time via {@link ng.$rootScope#applyAsync $rootScope.$applyAsync}. This can result in + * significant performance improvement for bigger applications that make many HTTP requests + * concurrently (common during application bootstrap). + * + * Defaults to false. If no value is specifed, returns the current configured value. + * + * @param {boolean=} value If true, when requests are loaded, they will schedule a deferred + * "apply" on the next tick, giving time for subsequent requests in a roughly ~10ms window + * to load and share the same digest cycle. + * + * @returns {boolean|Object} If a value is specified, returns the $httpProvider for chaining. + * otherwise, returns the current configured value. + **/ + this.useApplyAsync = function(value) { + if (isDefined(value)) { + useApplyAsync = !!value; + return this; + } + return useApplyAsync; + }; + /** * Are ordered by request, i.e. they are applied in the same order as the * array, on request, but reverse order, on response. */ var interceptorFactories = this.interceptors = []; - /** - * For historical reasons, response interceptors are ordered by the order in which - * they are applied to the response. (This is the opposite of interceptorFactories) - */ - var responseInterceptorFactories = this.responseInterceptors = []; - this.$get = ['$httpBackend', '$browser', '$cacheFactory', '$rootScope', '$q', '$injector', function($httpBackend, $browser, $cacheFactory, $rootScope, $q, $injector) { @@ -7477,27 +8020,6 @@ function $HttpProvider() { ? $injector.get(interceptorFactory) : $injector.invoke(interceptorFactory)); }); - forEach(responseInterceptorFactories, function(interceptorFactory, index) { - var responseFn = isString(interceptorFactory) - ? $injector.get(interceptorFactory) - : $injector.invoke(interceptorFactory); - - /** - * Response interceptors go before "around" interceptors (no real reason, just - * had to pick one.) But they are already reversed, so we can't use unshift, hence - * the splice. - */ - reversedInterceptors.splice(index, 0, { - response: function(response) { - return responseFn($q.when(response)); - }, - responseError: function(response) { - return responseFn($q.reject(response)); - } - }); - }); - - /** * @ngdoc service * @kind function @@ -7524,7 +8046,7 @@ function $HttpProvider() { * it is important to familiarize yourself with these APIs and the guarantees they provide. * * - * # General usage + * ## General usage * The `$http` service is a function which takes a single argument — a configuration object — * that is used to generate an HTTP request and returns a {@link ng.$q promise} * with two $http specific methods: `success` and `error`. @@ -7551,7 +8073,7 @@ function $HttpProvider() { * XMLHttpRequest will transparently follow it, meaning that the error callback will not be * called for such responses. * - * # Writing Unit Tests that use $http + * ## Writing Unit Tests that use $http * When unit testing (using {@link ngMock ngMock}), it is necessary to call * {@link ngMock.$httpBackend#flush $httpBackend.flush()} to flush each pending * request using trained responses. @@ -7562,7 +8084,7 @@ function $HttpProvider() { * $httpBackend.flush(); * ``` * - * # Shortcut methods + * ## Shortcut methods * * Shortcut methods are also available. All shortcut methods require passing in the URL, and * request data must be passed in for POST/PUT requests. @@ -7580,9 +8102,10 @@ function $HttpProvider() { * - {@link ng.$http#put $http.put} * - {@link ng.$http#delete $http.delete} * - {@link ng.$http#jsonp $http.jsonp} + * - {@link ng.$http#patch $http.patch} * * - * # Setting HTTP Headers + * ## Setting HTTP Headers * * The $http service will automatically add certain HTTP headers to all requests. These defaults * can be fully configured by accessing the `$httpProvider.defaults.headers` configuration @@ -7613,36 +8136,69 @@ function $HttpProvider() { * calling `$http(config)`, which overrides the defaults without changing them globally. * * - * # Transforming Requests and Responses + * ## Transforming Requests and Responses + * + * Both requests and responses can be transformed using transformation functions: `transformRequest` + * and `transformResponse`. These properties can be a single function that returns + * the transformed value (`{function(data, headersGetter)`) or an array of such transformation functions, + * which allows you to `push` or `unshift` a new transformation function into the transformation chain. + * + * ### Default Transformations + * + * The `$httpProvider` provider and `$http` service expose `defaults.transformRequest` and + * `defaults.transformResponse` properties. If a request does not provide its own transformations + * then these will be applied. + * + * You can augment or replace the default transformations by modifying these properties by adding to or + * replacing the array. * - * Both requests and responses can be transformed using transform functions. By default, Angular - * applies these transformations: + * Angular provides the following default transformations: * - * Request transformations: + * Request transformations (`$httpProvider.defaults.transformRequest` and `$http.defaults.transformRequest`): * * - If the `data` property of the request configuration object contains an object, serialize it * into JSON format. * - * Response transformations: + * Response transformations (`$httpProvider.defaults.transformResponse` and `$http.defaults.transformResponse`): * * - If XSRF prefix is detected, strip it (see Security Considerations section below). * - If JSON response is detected, deserialize it using a JSON parser. * - * To globally augment or override the default transforms, modify the - * `$httpProvider.defaults.transformRequest` and `$httpProvider.defaults.transformResponse` - * properties. These properties are by default an array of transform functions, which allows you - * to `push` or `unshift` a new transformation function into the transformation chain. You can - * also decide to completely override any default transformations by assigning your - * transformation functions to these properties directly without the array wrapper. These defaults - * are again available on the $http factory at run-time, which may be useful if you have run-time - * services you wish to be involved in your transformations. * - * Similarly, to locally override the request/response transforms, augment the - * `transformRequest` and/or `transformResponse` properties of the configuration object passed + * ### Overriding the Default Transformations Per Request + * + * If you wish override the request/response transformations only for a single request then provide + * `transformRequest` and/or `transformResponse` properties on the configuration object passed * into `$http`. * + * Note that if you provide these properties on the config object the default transformations will be + * overwritten. If you wish to augment the default transformations then you must include them in your + * local transformation array. + * + * The following code demonstrates adding a new response transformation to be run after the default response + * transformations have been run. + * + * ```js + * function appendTransform(defaults, transform) { + * + * // We can't guarantee that the default transformation is an array + * defaults = angular.isArray(defaults) ? defaults : [defaults]; + * + * // Append the new transformation to the defaults + * return defaults.concat(transform); + * } * - * # Caching + * $http({ + * url: '...', + * method: 'GET', + * transformResponse: appendTransform($http.defaults.transformResponse, function(value) { + * return doTransform(value); + * }) + * }); + * ``` + * + * + * ## Caching * * To enable caching, set the request configuration `cache` property to `true` (to use default * cache) or to a custom cache object (built with {@link ng.$cacheFactory `$cacheFactory`}). @@ -7665,7 +8221,7 @@ function $HttpProvider() { * If you set the default cache to `false` then only requests that specify their own custom * cache object will be cached. * - * # Interceptors + * ## Interceptors * * Before you start creating interceptors, be sure to understand the * {@link ng.$q $q and deferred/promise APIs}. @@ -7750,52 +8306,7 @@ function $HttpProvider() { * }); * ``` * - * # Response interceptors (DEPRECATED) - * - * Before you start creating interceptors, be sure to understand the - * {@link ng.$q $q and deferred/promise APIs}. - * - * For purposes of global error handling, authentication or any kind of synchronous or - * asynchronous preprocessing of received responses, it is desirable to be able to intercept - * responses for http requests before they are handed over to the application code that - * initiated these requests. The response interceptors leverage the {@link ng.$q - * promise apis} to fulfil this need for both synchronous and asynchronous preprocessing. - * - * The interceptors are service factories that are registered with the $httpProvider by - * adding them to the `$httpProvider.responseInterceptors` array. The factory is called and - * injected with dependencies (if specified) and returns the interceptor — a function that - * takes a {@link ng.$q promise} and returns the original or a new promise. - * - * ```js - * // register the interceptor as a service - * $provide.factory('myHttpInterceptor', function($q, dependency1, dependency2) { - * return function(promise) { - * return promise.then(function(response) { - * // do something on success - * return response; - * }, function(response) { - * // do something on error - * if (canRecover(response)) { - * return responseOrNewPromise - * } - * return $q.reject(response); - * }); - * } - * }); - * - * $httpProvider.responseInterceptors.push('myHttpInterceptor'); - * - * - * // register the interceptor via an anonymous factory - * $httpProvider.responseInterceptors.push(function($q, dependency1, dependency2) { - * return function(promise) { - * // same as above - * } - * }); - * ``` - * - * - * # Security Considerations + * ## Security Considerations * * When designing web applications, consider security threats from: * @@ -7806,7 +8317,7 @@ function $HttpProvider() { * pre-configured with strategies that address these issues, but for this to work backend server * cooperation is required. * - * ## JSON Vulnerability Protection + * ### JSON Vulnerability Protection * * A [JSON vulnerability](http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx) * allows third party website to turn your JSON resource URL into @@ -7828,7 +8339,7 @@ function $HttpProvider() { * Angular will strip the prefix, before processing the JSON. * * - * ## Cross Site Request Forgery (XSRF) Protection + * ### Cross Site Request Forgery (XSRF) Protection * * [XSRF](http://en.wikipedia.org/wiki/Cross-site_request_forgery) is a technique by which * an unauthorized site can gain your user's private data. Angular provides a mechanism @@ -7844,7 +8355,7 @@ function $HttpProvider() { * that only JavaScript running on your domain could have sent the request. The token must be * unique for each user and must be verifiable by the server (to prevent the JavaScript from * making up its own tokens). We recommend that the token is a digest of your site's - * authentication cookie with a [salt](https://en.wikipedia.org/wiki/Salt_(cryptography)) + * authentication cookie with a [salt](https://en.wikipedia.org/wiki/Salt_(cryptography)) * for added security. * * The name of the headers can be specified using the xsrfHeaderName and xsrfCookieName @@ -7870,10 +8381,12 @@ function $HttpProvider() { * `{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. + * See {@link #overriding-the-default-transformations-per-request Overriding the Default Transformations} * - **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. + * See {@link #overriding-the-default-transformations-per-request Overriding the Default Transformations} * - **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 @@ -7881,7 +8394,7 @@ function $HttpProvider() { * - **timeout** – `{number|Promise}` – timeout in milliseconds, or {@link ng.$q promise} * that should abort the request when resolved. * - **withCredentials** - `{boolean}` - whether to set the `withCredentials` flag on the - * XHR object. See [requests with credentials]https://developer.mozilla.org/en/http_access_control#section_5 + * XHR object. See [requests with credentials](https://developer.mozilla.org/docs/Web/HTTP/Access_control_CORS#Requests_with_credentials) * for more information. * - **responseType** - `{string}` - see * [requestType](https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#responseType). @@ -8159,7 +8672,7 @@ function $HttpProvider() { * Shortcut method to perform `JSONP` request. * * @param {string} url Relative or absolute URL specifying the destination of the request. - * Should contain `JSON_CALLBACK` string. + * The name of the callback should be the string `JSON_CALLBACK`. * @param {Object=} config Optional configuration object * @returns {HttpPromise} Future object */ @@ -8190,7 +8703,20 @@ function $HttpProvider() { * @param {Object=} config Optional configuration object * @returns {HttpPromise} Future object */ - createShortMethodsWithData('post', 'put'); + + /** + * @ngdoc method + * @name $http#patch + * + * @description + * Shortcut method to perform `PATCH` request. + * + * @param {string} url Relative or absolute URL specifying the destination of the request + * @param {*} data Request content + * @param {Object=} config Optional configuration object + * @returns {HttpPromise} Future object + */ + createShortMethodsWithData('post', 'put', 'patch'); /** * @ngdoc property @@ -8250,7 +8776,8 @@ function $HttpProvider() { promise.then(removePendingReq, removePendingReq); - if ((config.cache || defaults.cache) && config.cache !== false && config.method == 'GET') { + if ((config.cache || defaults.cache) && config.cache !== false && + (config.method === 'GET' || config.method === 'JSONP')) { cache = isObject(config.cache) ? config.cache : isObject(defaults.cache) ? defaults.cache : defaultCache; @@ -8259,7 +8786,7 @@ function $HttpProvider() { if (cache) { cachedResp = cache.get(url); if (isDefined(cachedResp)) { - if (cachedResp.then) { + if (isPromiseLike(cachedResp)) { // cached request has already been sent, but there is no response yet cachedResp.then(removePendingReq, removePendingReq); return cachedResp; @@ -8311,8 +8838,16 @@ function $HttpProvider() { } } - resolvePromise(response, status, headersString, statusText); - if (!$rootScope.$$phase) $rootScope.$apply(); + function resolveHttpPromise() { + resolvePromise(response, status, headersString, statusText); + } + + if (useApplyAsync) { + $rootScope.$applyAsync(resolveHttpPromise); + } else { + resolveHttpPromise(); + if (!$rootScope.$$phase) $rootScope.$apply(); + } } @@ -8334,34 +8869,36 @@ function $HttpProvider() { function removePendingReq() { - var idx = indexOf($http.pendingRequests, config); + var idx = $http.pendingRequests.indexOf(config); if (idx !== -1) $http.pendingRequests.splice(idx, 1); } } function buildUrl(url, params) { - if (!params) return url; - var parts = []; - forEachSorted(params, function(value, key) { - if (value === null || isUndefined(value)) return; - if (!isArray(value)) value = [value]; - - forEach(value, function(v) { - if (isObject(v)) { - v = toJson(v); - } - parts.push(encodeUriQuery(key) + '=' + - encodeUriQuery(v)); - }); - }); - if(parts.length > 0) { - url += ((url.indexOf('?') == -1) ? '?' : '&') + parts.join('&'); + if (!params) return url; + var parts = []; + forEachSorted(params, function(value, key) { + if (value === null || isUndefined(value)) return; + if (!isArray(value)) value = [value]; + + forEach(value, function(v) { + if (isObject(v)) { + if (isDate(v)){ + v = v.toISOString(); + } else { + v = toJson(v); + } } - return url; - } - - + parts.push(encodeUriQuery(key) + '=' + + encodeUriQuery(v)); + }); + }); + if(parts.length > 0) { + url += ((url.indexOf('?') == -1) ? '?' : '&') + parts.join('&'); + } + return url; + } }]; } @@ -8497,7 +9034,7 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc if (timeout > 0) { var timeoutId = $browserDefer(timeoutRequest, timeout); - } else if (timeout && timeout.then) { + } else if (isPromiseLike(timeout)) { timeout.then(timeoutRequest); } @@ -8561,18 +9098,6 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc addEventListenerFn(script, "load", callback); addEventListenerFn(script, "error", callback); - - if (msie <= 8) { - script.onreadystatechange = function() { - if (isString(script.readyState) && /loaded|complete/.test(script.readyState)) { - script.onreadystatechange = null; - callback({ - type: 'load' - }); - } - }; - } - rawDocument.body.appendChild(script); return callback; } @@ -8583,7 +9108,6 @@ var $interpolateMinErr = minErr('$interpolate'); /** * @ngdoc provider * @name $interpolateProvider - * @kind function * * @description * @@ -8659,7 +9183,13 @@ function $InterpolateProvider() { this.$get = ['$parse', '$exceptionHandler', '$sce', function($parse, $exceptionHandler, $sce) { var startSymbolLength = startSymbol.length, - endSymbolLength = endSymbol.length; + endSymbolLength = endSymbol.length, + escapedStartRegexp = new RegExp(startSymbol.replace(/./g, escape), 'g'), + escapedEndRegexp = new RegExp(endSymbol.replace(/./g, escape), 'g'); + + function escape(ch) { + return '\\\\\\' + ch; + } /** * @ngdoc service @@ -8683,6 +9213,62 @@ function $InterpolateProvider() { * expect(exp({name:'Angular'}).toEqual('Hello ANGULAR!'); * ``` * + * `$interpolate` takes an optional fourth argument, `allOrNothing`. If `allOrNothing` is + * `true`, the interpolation function will return `undefined` unless all embedded expressions + * evaluate to a value other than `undefined`. + * + * ```js + * var $interpolate = ...; // injected + * var context = {greeting: 'Hello', name: undefined }; + * + * // default "forgiving" mode + * var exp = $interpolate('{{greeting}} {{name}}!'); + * expect(exp(context)).toEqual('Hello !'); + * + * // "allOrNothing" mode + * exp = $interpolate('{{greeting}} {{name}}!', false, null, true); + * expect(exp(context)).toBeUndefined(); + * context.name = 'Angular'; + * expect(exp(context)).toEqual('Hello Angular!'); + * ``` + * + * `allOrNothing` is useful for interpolating URLs. `ngSrc` and `ngSrcset` use this behavior. + * + * ####Escaped Interpolation + * $interpolate provides a mechanism for escaping interpolation markers. Start and end markers + * can be escaped by preceding each of their characters with a REVERSE SOLIDUS U+005C (backslash). + * It will be rendered as a regular start/end marker, and will not be interpreted as an expression + * or binding. + * + * This enables web-servers to prevent script injection attacks and defacing attacks, to some + * degree, while also enabling code examples to work without relying on the + * {@link ng.directive:ngNonBindable ngNonBindable} directive. + * + * **For security purposes, it is strongly encouraged that web servers escape user-supplied data, + * replacing angle brackets (<, >) with &lt; and &gt; respectively, and replacing all + * interpolation start/end markers with their escaped counterparts.** + * + * Escaped interpolation markers are only replaced with the actual interpolation markers in rendered + * output when the $interpolate service processes the text. So, for HTML elements interpolated + * by {@link ng.$compile $compile}, or otherwise interpolated with the `mustHaveExpression` parameter + * set to `true`, the interpolated text must contain an unescaped interpolation expression. As such, + * this is typically useful only when user-data is used in rendering a template from the server, or + * when otherwise untrusted data is used by a directive. + * + * <example> + * <file name="index.html"> + * <div ng-init="username='A user'"> + * <p ng-init="apptitle='Escaping demo'">{{apptitle}}: \{\{ username = "defaced value"; \}\} + * </p> + * <p><strong>{{username}}</strong> attempts to inject code which will deface the + * application, but fails to accomplish their task, because the server has correctly + * escaped the interpolation start/end markers with REVERSE SOLIDUS U+005C (backslash) + * characters.</p> + * <p>Instead, the result of the attempted script injection is visible, and can be removed + * from the database by an administrator.</p> + * </div> + * </file> + * </example> * * @param {string} text The text with markup to interpolate. * @param {boolean=} mustHaveExpression if set to true then the interpolation string must have @@ -8692,103 +9278,141 @@ function $InterpolateProvider() { * result through {@link ng.$sce#getTrusted $sce.getTrusted(interpolatedResult, * trustedContext)} before returning it. Refer to the {@link ng.$sce $sce} service that * provides Strict Contextual Escaping for details. + * @param {boolean=} allOrNothing if `true`, then the returned function returns undefined + * unless all embedded expressions evaluate to a value other than `undefined`. * @returns {function(context)} an interpolation function which is used to compute the * interpolated string. The function has these parameters: * - * * `context`: an object against which any expressions embedded in the strings are evaluated - * against. - * + * - `context`: evaluation context for all expressions embedded in the interpolated text */ - function $interpolate(text, mustHaveExpression, trustedContext) { + function $interpolate(text, mustHaveExpression, trustedContext, allOrNothing) { + allOrNothing = !!allOrNothing; var startIndex, endIndex, index = 0, - parts = [], - length = text.length, - hasInterpolation = false, - fn, + expressions = [], + parseFns = [], + textLength = text.length, exp, - concat = []; + concat = [], + expressionPositions = []; - while(index < length) { + while(index < textLength) { if ( ((startIndex = text.indexOf(startSymbol, index)) != -1) && ((endIndex = text.indexOf(endSymbol, startIndex + startSymbolLength)) != -1) ) { - (index != startIndex) && parts.push(text.substring(index, startIndex)); - parts.push(fn = $parse(exp = text.substring(startIndex + startSymbolLength, endIndex))); - fn.exp = exp; + if (index !== startIndex) { + concat.push(unescapeText(text.substring(index, startIndex))); + } + exp = text.substring(startIndex + startSymbolLength, endIndex); + expressions.push(exp); + parseFns.push($parse(exp, parseStringifyInterceptor)); index = endIndex + endSymbolLength; - hasInterpolation = true; + expressionPositions.push(concat.length); + concat.push(''); } else { - // we did not find anything, so we have to add the remainder to the parts array - (index != length) && parts.push(text.substring(index)); - index = length; + // we did not find an interpolation, so we have to add the remainder to the separators array + if (index !== textLength) { + concat.push(unescapeText(text.substring(index))); + } + break; } } - if (!(length = parts.length)) { - // we added, nothing, must have been an empty string. - parts.push(''); - length = 1; - } - // Concatenating expressions makes it hard to reason about whether some combination of // concatenated values are unsafe to use and could easily lead to XSS. By requiring that a // single expression be used for iframe[src], object[src], etc., we ensure that the value // that's used is assigned or constructed by some JS code somewhere that is more testable or // make it obvious that you bound the value to some user controlled value. This helps reduce // the load when auditing for XSS issues. - if (trustedContext && parts.length > 1) { + if (trustedContext && concat.length > 1) { throw $interpolateMinErr('noconcat', "Error while interpolating: {0}\nStrict Contextual Escaping disallows " + "interpolations that concatenate multiple expressions when a trusted value is " + "required. See http://docs.angularjs.org/api/ng.$sce", text); } - if (!mustHaveExpression || hasInterpolation) { - concat.length = length; - fn = function(context) { - try { - for(var i = 0, ii = length, part; i<ii; i++) { - if (typeof (part = parts[i]) == 'function') { - part = part(context); - if (trustedContext) { - part = $sce.getTrusted(trustedContext, part); - } else { - part = $sce.valueOf(part); - } - if (part == null) { // null || undefined - part = ''; - } else { - switch (typeof part) { - case 'string': - { - break; - } - case 'number': - { - part = '' + part; - break; - } - default: - { - part = toJson(part); - } - } - } - } - concat[i] = part; - } - return concat.join(''); + if (!mustHaveExpression || expressions.length) { + var compute = function(values) { + for(var i = 0, ii = expressions.length; i < ii; i++) { + if (allOrNothing && isUndefined(values[i])) return; + concat[expressionPositions[i]] = values[i]; } - catch(err) { - var newErr = $interpolateMinErr('interr', "Can't interpolate: {0}\n{1}", text, - err.toString()); - $exceptionHandler(newErr); + return concat.join(''); + }; + + var getValue = function (value) { + return trustedContext ? + $sce.getTrusted(trustedContext, value) : + $sce.valueOf(value); + }; + + var stringify = function (value) { + if (value == null) { // null || undefined + return ''; } + switch (typeof value) { + case 'string': { + break; + } + case 'number': { + value = '' + value; + break; + } + default: { + value = toJson(value); + } + } + + return value; }; - fn.exp = text; - fn.parts = parts; - return fn; + + return extend(function interpolationFn(context) { + var i = 0; + var ii = expressions.length; + var values = new Array(ii); + + try { + for (; i < ii; i++) { + values[i] = parseFns[i](context); + } + + return compute(values); + } catch(err) { + var newErr = $interpolateMinErr('interr', "Can't interpolate: {0}\n{1}", text, + err.toString()); + $exceptionHandler(newErr); + } + + }, { + // all of these properties are undocumented for now + exp: text, //just for compatibility with regular watchers created via $watch + expressions: expressions, + $$watchDelegate: function (scope, listener, objectEquality) { + var lastValue; + return scope.$watchGroup(parseFns, function interpolateFnWatcher(values, oldValues) { + var currValue = compute(values); + if (isFunction(listener)) { + listener.call(this, currValue, values !== oldValues ? lastValue : currValue, scope); + } + lastValue = currValue; + }, objectEquality); + } + }); + } + + function unescapeText(text) { + return text.replace(escapedStartRegexp, startSymbol). + replace(escapedEndRegexp, endSymbol); + } + + function parseStringifyInterceptor(value) { + try { + return stringify(getValue(value)); + } catch(err) { + var newErr = $interpolateMinErr('interr', "Can't interpolate: {0}\n{1}", text, + err.toString()); + $exceptionHandler(newErr); + } } } @@ -8799,7 +9423,7 @@ function $InterpolateProvider() { * @description * Symbol to denote the start of expression in the interpolated string. Defaults to `{{`. * - * Use {@link ng.$interpolateProvider#startSymbol $interpolateProvider#startSymbol} to change + * Use {@link ng.$interpolateProvider#startSymbol `$interpolateProvider.startSymbol`} to change * the symbol. * * @returns {string} start symbol. @@ -8815,7 +9439,7 @@ function $InterpolateProvider() { * @description * Symbol to denote the end of expression in the interpolated string. Defaults to `}}`. * - * Use {@link ng.$interpolateProvider#endSymbol $interpolateProvider#endSymbol} to change + * Use {@link ng.$interpolateProvider#endSymbol `$interpolateProvider.endSymbol`} to change * the symbol. * * @returns {string} end symbol. @@ -8829,8 +9453,8 @@ function $InterpolateProvider() { } function $IntervalProvider() { - this.$get = ['$rootScope', '$window', '$q', - function($rootScope, $window, $q) { + this.$get = ['$rootScope', '$window', '$q', '$$q', + function($rootScope, $window, $q, $$q) { var intervals = {}; @@ -8907,10 +9531,10 @@ function $IntervalProvider() { * }; * * $scope.$on('$destroy', function() { - * // Make sure that the interval nis destroyed too + * // Make sure that the interval is destroyed too * $scope.stopFight(); * }); - * }) + * }]) * // Register the 'myCurrentTime' directive factory method. * // We inject $interval and dateFilter service since the factory method is DI. * .directive('myCurrentTime', ['$interval', 'dateFilter', @@ -8935,11 +9559,11 @@ function $IntervalProvider() { * * // listen on DOM destroy (removal) event, and cancel the next UI update * // to prevent updating time after the DOM element was removed. - * element.bind('$destroy', function() { + * element.on('$destroy', function() { * $interval.cancel(stopTime); * }); * } - * }); + * }]); * </script> * * <div> @@ -8961,10 +9585,10 @@ function $IntervalProvider() { function interval(fn, delay, count, invokeApply) { var setInterval = $window.setInterval, clearInterval = $window.clearInterval, - deferred = $q.defer(), - promise = deferred.promise, iteration = 0, - skipApply = (isDefined(invokeApply) && !invokeApply); + skipApply = (isDefined(invokeApply) && !invokeApply), + deferred = (skipApply ? $$q : $q).defer(), + promise = deferred.promise; count = isDefined(count) ? count : 0; @@ -9212,21 +9836,32 @@ function LocationHtml5Url(appBase, basePrefix) { this.$$absUrl = appBaseNoFile + this.$$url.substr(1); // first char is always '/' }; - this.$$rewrite = function(url) { + this.$$parseLinkUrl = function(url, relHref) { + if (relHref && relHref[0] === '#') { + // special case for links to hash fragments: + // keep the old url and only replace the hash fragment + this.hash(relHref.slice(1)); + return true; + } var appUrl, prevAppUrl; + var rewrittenUrl; if ( (appUrl = beginsWith(appBase, url)) !== undefined ) { prevAppUrl = appUrl; if ( (appUrl = beginsWith(basePrefix, appUrl)) !== undefined ) { - return appBaseNoFile + (beginsWith('/', appUrl) || appUrl); + rewrittenUrl = appBaseNoFile + (beginsWith('/', appUrl) || appUrl); } else { - return appBase + prevAppUrl; + rewrittenUrl = appBase + prevAppUrl; } } else if ( (appUrl = beginsWith(appBaseNoFile, url)) !== undefined ) { - return appBaseNoFile + appUrl; + rewrittenUrl = appBaseNoFile + appUrl; } else if (appBaseNoFile == url + '/') { - return appBaseNoFile; + rewrittenUrl = appBaseNoFile; } + if (rewrittenUrl) { + this.$$parse(rewrittenUrl); + } + return !!rewrittenUrl; }; } @@ -9316,10 +9951,12 @@ function LocationHashbangUrl(appBase, hashPrefix) { this.$$absUrl = appBase + (this.$$url ? hashPrefix + this.$$url : ''); }; - this.$$rewrite = function(url) { + this.$$parseLinkUrl = function(url, relHref) { if(stripHash(appBase) == stripHash(url)) { - return url; + this.$$parse(url); + return true; } + return false; }; } @@ -9339,16 +9976,28 @@ function LocationHashbangInHtml5Url(appBase, hashPrefix) { var appBaseNoFile = stripFile(appBase); - this.$$rewrite = function(url) { + this.$$parseLinkUrl = function(url, relHref) { + if (relHref && relHref[0] === '#') { + // special case for links to hash fragments: + // keep the old url and only replace the hash fragment + this.hash(relHref.slice(1)); + return true; + } + + var rewrittenUrl; var appUrl; if ( appBase == stripHash(url) ) { - return url; + rewrittenUrl = url; } else if ( (appUrl = beginsWith(appBaseNoFile, url)) ) { - return appBase + hashPrefix + appUrl; + rewrittenUrl = appBase + hashPrefix + appUrl; } else if ( appBaseNoFile === url + '/') { - return appBaseNoFile; + rewrittenUrl = appBaseNoFile; } + if (rewrittenUrl) { + this.$$parse(rewrittenUrl); + } + return !!rewrittenUrl; }; this.$$compose = function() { @@ -9405,17 +10054,16 @@ LocationHashbangInHtml5Url.prototype = * Change path, search and hash, when called with parameter and return `$location`. * * @param {string=} url New url without base prefix (e.g. `/path?a=b#hash`) - * @param {string=} replace The path that will be changed * @return {string} url */ - url: function(url, replace) { + url: function(url) { if (isUndefined(url)) return this.$$url; var match = PATH_MATCH.exec(url); if (match[1]) this.path(decodeURIComponent(match[1])); if (match[2] || match[1]) this.search(match[3] || ''); - this.hash(match[5] || '', replace); + this.hash(match[5] || ''); return this; }, @@ -9473,10 +10121,11 @@ LocationHashbangInHtml5Url.prototype = * Note: Path should always begin with forward slash (/), this method will add the forward slash * if it is missing. * - * @param {string=} path New path + * @param {(string|number)=} path New path * @return {string} path */ path: locationGetterSetter('$$path', function(path) { + path = path ? path.toString() : ''; return path.charAt(0) == '/' ? path : '/' + path; }), @@ -9512,7 +10161,7 @@ LocationHashbangInHtml5Url.prototype = * If the argument is a hash object containing an array of values, these values will be encoded * as duplicate search parameters in the url. * - * @param {(string|Array<string>|boolean)=} paramValue If `search` is a string, then `paramValue` + * @param {(string|Number|Array<string>|boolean)=} paramValue If `search` is a string or number, then `paramValue` * will override only a single search property. * * If `paramValue` is an array, it will override the property of the `search` component of @@ -9531,7 +10180,8 @@ LocationHashbangInHtml5Url.prototype = case 0: return this.$$search; case 1: - if (isString(search)) { + if (isString(search) || isNumber(search)) { + search = search.toString(); this.$$search = parseKeyValue(search); } else if (isObject(search)) { // remove object undefined or null properties @@ -9568,10 +10218,12 @@ LocationHashbangInHtml5Url.prototype = * * Change hash fragment when called with parameter and return `$location`. * - * @param {string=} hash New hash fragment + * @param {(string|number)=} hash New hash fragment * @return {string} hash */ - hash: locationGetterSetter('$$hash', identity), + hash: locationGetterSetter('$$hash', function(hash) { + return hash ? hash.toString() : ''; + }), /** * @ngdoc method @@ -9711,6 +10363,10 @@ function $LocationProvider(){ appBase; if (html5Mode) { + if (!baseHref) { + throw $locationMinErr('nobase', + "$location in HTML5 mode requires a <base> tag to be present!"); + } appBase = serverBase(initialUrl) + (baseHref || '/'); LocationMode = $sniffer.history ? LocationHtml5Url : LocationHashbangInHtml5Url; } else { @@ -9718,7 +10374,9 @@ function $LocationProvider(){ LocationMode = LocationHashbangUrl; } $location = new LocationMode(appBase, '#' + hashPrefix); - $location.$$parse($location.$$rewrite(initialUrl)); + $location.$$parseLinkUrl(initialUrl, initialUrl); + + var IGNORE_URI_REGEXP = /^\s*(javascript|mailto):/i; $rootElement.on('click', function(event) { // TODO(vojta): rewrite link when opening in new tab/window (in legacy browser) @@ -9729,12 +10387,15 @@ function $LocationProvider(){ var elm = jqLite(event.target); // traverse the DOM up to find first A tag - while (lowercase(elm[0].nodeName) !== 'a') { + while (nodeName_(elm[0]) !== 'a') { // ignore rewriting if no A tag (reached root element, or no parent - removed from document) if (elm[0] === $rootElement[0] || !(elm = elm.parent())[0]) return; } var absHref = elm.prop('href'); + // get the actual href attribute - see + // http://msdn.microsoft.com/en-us/library/ie/dd347148(v=vs.85).aspx + var relHref = elm.attr('href') || elm.attr('xlink:href'); if (isObject(absHref) && absHref.toString() === '[object SVGAnimatedString]') { // SVGAnimatedString.animVal should be identical to SVGAnimatedString.baseVal, unless during @@ -9742,49 +10403,18 @@ function $LocationProvider(){ absHref = urlResolve(absHref.animVal).href; } - // Make relative links work in HTML5 mode for legacy browsers (or at least IE8 & 9) - // The href should be a regular url e.g. /link/somewhere or link/somewhere or ../somewhere or - // somewhere#anchor or http://example.com/somewhere - if (LocationMode === LocationHashbangInHtml5Url) { - // get the actual href attribute - see - // http://msdn.microsoft.com/en-us/library/ie/dd347148(v=vs.85).aspx - var href = elm.attr('href') || elm.attr('xlink:href'); - - if (href.indexOf('://') < 0) { // Ignore absolute URLs - var prefix = '#' + hashPrefix; - if (href[0] == '/') { - // absolute path - replace old path - absHref = appBase + prefix + href; - } else if (href[0] == '#') { - // local anchor - absHref = appBase + prefix + ($location.path() || '/') + href; - } else { - // relative path - join with current path - var stack = $location.path().split("/"), - parts = href.split("/"); - for (var i=0; i<parts.length; i++) { - if (parts[i] == ".") - continue; - else if (parts[i] == "..") - stack.pop(); - else if (parts[i].length) - stack.push(parts[i]); - } - absHref = appBase + prefix + stack.join('/'); - } - } - } - - var rewrittenUrl = $location.$$rewrite(absHref); + // Ignore when url is started with javascript: or mailto: + if (IGNORE_URI_REGEXP.test(absHref)) return; - if (absHref && !elm.attr('target') && rewrittenUrl && !event.isDefaultPrevented()) { - event.preventDefault(); - if (rewrittenUrl != $browser.url()) { + if (absHref && !elm.attr('target') && !event.isDefaultPrevented()) { + if ($location.$$parseLinkUrl(absHref, relHref)) { + event.preventDefault(); // update location manually - $location.$$parse(rewrittenUrl); - $rootScope.$apply(); - // hack to work around FF6 bug 684208 when scenario runner clicks on links - window.angular['ff-684208-preventDefault'] = true; + if ($location.absUrl() != $browser.url()) { + $rootScope.$apply(); + // hack to work around FF6 bug 684208 when scenario runner clicks on links + window.angular['ff-684208-preventDefault'] = true; + } } } }); @@ -10008,8 +10638,6 @@ function $LogProvider(){ } var $parseMinErr = minErr('$parse'); -var promiseWarningCache = {}; -var promiseWarning; // Sandboxing Angular Expressions // ------------------------------ @@ -10052,7 +10680,7 @@ function ensureSafeObject(obj, fullExpression) { 'Referencing Function in Angular expressions is disallowed! Expression: {0}', fullExpression); } else if (// isWindow(obj) - obj.document && obj.location && obj.alert && obj.setInterval) { + obj.window === obj) { throw $parseMinErr('isecwindow', 'Referencing the Window in Angular expressions is disallowed! Expression: {0}', fullExpression); @@ -10081,7 +10709,7 @@ function ensureSafeFunction(obj, fullExpression) { throw $parseMinErr('isecfn', 'Referencing Function in Angular expressions is disallowed! Expression: {0}', fullExpression); - } else if (obj === CALL || obj === APPLY || (BIND && obj === BIND)) { + } else if (obj === CALL || obj === APPLY || obj === BIND) { throw $parseMinErr('isecff', 'Referencing call, apply or bind in Angular expressions is disallowed! Expression: {0}', fullExpression); @@ -10089,12 +10717,21 @@ function ensureSafeFunction(obj, fullExpression) { } } -var OPERATORS = { +//Keyword constants +var CONSTANTS = createMap(); +forEach({ + 'null': function() { return null; }, + 'true': function() { return true; }, + 'false': function() { return false; }, + 'undefined': function() {} +}, function(constantGetter, name) { + constantGetter.constant = constantGetter.literal = constantGetter.sharedGetter = true; + CONSTANTS[name] = constantGetter; +}); + +//Operators - will be wrapped by binaryFn/unaryFn/assignment/filter +var OPERATORS = extend(createMap(), { /* jshint bitwise : false */ - 'null':function(){return null;}, - 'true':function(){return true;}, - 'false':function(){return false;}, - undefined:noop, '+':function(self, locals, a,b){ a=a(self, locals); b=b(self, locals); if (isDefined(a)) { @@ -10127,7 +10764,7 @@ var OPERATORS = { // '|':function(self, locals, a,b){return a|b;}, '|':function(self, locals, a,b){return b(self, locals)(self, locals, a(self, locals));}, '!':function(self, locals, a){return !a(self, locals);} -}; +}); /* jshint bitwise: true */ var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'}; @@ -10147,11 +10784,8 @@ Lexer.prototype = { lex: function (text) { this.text = text; - this.index = 0; this.ch = undefined; - this.lastCh = ':'; // can start regexp - this.tokens = []; while (this.index < this.text.length) { @@ -10170,7 +10804,6 @@ Lexer.prototype = { this.index++; } else if (this.isWhitespace(this.ch)) { this.index++; - continue; } else { var ch2 = this.ch + this.peek(); var ch3 = ch2 + this.peek(2); @@ -10194,7 +10827,6 @@ Lexer.prototype = { this.throwError('Unexpected next character ', this.index, this.index + 1); } } - this.lastCh = this.ch; } return this.tokens; }, @@ -10203,10 +10835,6 @@ Lexer.prototype = { return chars.indexOf(this.ch) !== -1; }, - was: function(chars) { - return chars.indexOf(this.lastCh) !== -1; - }, - peek: function(i) { var num = i || 1; return (this.index + num < this.text.length) ? this.text.charAt(this.index + num) : false; @@ -10270,14 +10898,13 @@ Lexer.prototype = { this.tokens.push({ index: start, text: number, - literal: true, constant: true, fn: function() { return number; } }); }, readIdent: function() { - var parser = this; + var expression = this.text; var ident = ''; var start = this.index; @@ -10295,6 +10922,16 @@ Lexer.prototype = { this.index++; } + //check if the identifier ends with . and if so move back one char + if (lastDot && ident[ident.length - 1] === '.') { + this.index--; + ident = ident.slice(0, -1); + lastDot = ident.lastIndexOf('.'); + if (lastDot === -1) { + lastDot = undefined; + } + } + //check if this is not a method invocation and if it is back out to last dot if (lastDot) { peekIndex = this.index; @@ -10314,33 +10951,15 @@ Lexer.prototype = { } } - - var token = { + this.tokens.push({ index: start, - text: ident - }; - - // OPERATORS is our own object so we don't need to use special hasOwnPropertyFn - if (OPERATORS.hasOwnProperty(ident)) { - token.fn = OPERATORS[ident]; - token.literal = true; - token.constant = true; - } else { - var getter = getterFn(ident, this.options, this.text); - token.fn = extend(function(self, locals) { - return (getter(self, locals)); - }, { - assign: function(self, value) { - return setter(self, ident, value, parser.text, parser.options); - } - }); - } - - this.tokens.push(token); + text: ident, + fn: CONSTANTS[ident] || getterFn(ident, this.options, expression) + }); if (methodName) { this.tokens.push({ - index:lastDot, + index: lastDot, text: '.' }); this.tokens.push({ @@ -10368,11 +10987,7 @@ Lexer.prototype = { string += String.fromCharCode(parseInt(hex, 16)); } else { var rep = ESCAPE[ch]; - if (rep) { - string += rep; - } else { - string += ch; - } + string = string + (rep || ch); } escape = false; } else if (ch === '\\') { @@ -10383,7 +10998,6 @@ Lexer.prototype = { index: start, text: rawString, string: string, - literal: true, constant: true, fn: function() { return string; } }); @@ -10410,6 +11024,7 @@ var Parser = function (lexer, $filter, options) { Parser.ZERO = extend(function () { return 0; }, { + sharedGetter: true, constant: true }); @@ -10418,7 +11033,6 @@ Parser.prototype = { parse: function (text) { this.text = text; - this.tokens = this.lexer.lex(text); var value = this.statements(); @@ -10448,8 +11062,10 @@ Parser.prototype = { if (!primary) { this.throwError('not a primary expression', token); } - primary.literal = !!token.literal; - primary.constant = !!token.constant; + if (token.constant) { + primary.constant = true; + primary.literal = true; + } } var next, context; @@ -10543,13 +11159,10 @@ Parser.prototype = { // TODO(size): maybe we should not support multiple statements? return (statements.length === 1) ? statements[0] - : function(self, locals) { + : function $parseStatements(self, locals) { var value; - for (var i = 0; i < statements.length; i++) { - var statement = statements[i]; - if (statement) { - value = statement(self, locals); - } + for (var i = 0, ii = statements.length; i < ii; i++) { + value = statements[i](self, locals); } return value; }; @@ -10560,35 +11173,40 @@ Parser.prototype = { filterChain: function() { var left = this.expression(); var token; - while (true) { - if ((token = this.expect('|'))) { - left = this.binaryFn(left, token.fn, this.filter()); - } else { - return left; - } + while ((token = this.expect('|'))) { + left = this.binaryFn(left, token.fn, this.filter()); } + return left; }, filter: function() { var token = this.expect(); var fn = this.$filter(token.text); - var argsFn = []; - while (true) { - if ((token = this.expect(':'))) { + var argsFn; + var args; + + if (this.peek(':')) { + argsFn = []; + args = []; // we can safely reuse the array + while (this.expect(':')) { argsFn.push(this.expression()); - } else { - var fnInvoke = function(self, locals, input) { - var args = [input]; - for (var i = 0; i < argsFn.length; i++) { - args.push(argsFn[i](self, locals)); - } - return fn.apply(self, args); - }; - return function() { - return fnInvoke; - }; } } + + return valueFn(function $parseFilter(self, locals, input) { + if (args) { + args[0] = input; + + var i = argsFn.length; + while (i--) { + args[i + 1] = argsFn[i](self, locals); + } + + return fn.apply(undefined, args); + } + + return fn(input); + }); }, expression: function() { @@ -10605,7 +11223,7 @@ Parser.prototype = { this.text.substring(0, token.index) + '] can not be assigned to', token); } right = this.ternary(); - return function(scope, locals) { + return function $parseAssignment(scope, locals) { return left.assign(scope, right(scope, locals), locals); }; } @@ -10617,9 +11235,9 @@ Parser.prototype = { var middle; var token; if ((token = this.expect('?'))) { - middle = this.ternary(); + middle = this.assignment(); if ((token = this.expect(':'))) { - return this.ternaryFn(left, middle, this.ternary()); + return this.ternaryFn(left, middle, this.assignment()); } else { this.throwError('expected :', token); } @@ -10631,13 +11249,10 @@ Parser.prototype = { logicalOR: function() { var left = this.logicalAND(); var token; - while (true) { - if ((token = this.expect('||'))) { - left = this.binaryFn(left, token.fn, this.logicalAND()); - } else { - return left; - } + while ((token = this.expect('||'))) { + left = this.binaryFn(left, token.fn, this.logicalAND()); } + return left; }, logicalAND: function() { @@ -10699,53 +11314,48 @@ Parser.prototype = { }, fieldAccess: function(object) { - var parser = this; + var expression = this.text; var field = this.expect().text; - var getter = getterFn(field, this.options, this.text); + var getter = getterFn(field, this.options, expression); - return extend(function(scope, locals, self) { + return extend(function $parseFieldAccess(scope, locals, self) { return getter(self || object(scope, locals)); }, { assign: function(scope, value, locals) { - return setter(object(scope, locals), field, value, parser.text, parser.options); + var o = object(scope, locals); + if (!o) object.assign(scope, o = {}); + return setter(o, field, value, expression); } }); }, objectIndex: function(obj) { - var parser = this; + var expression = this.text; var indexFn = this.expression(); this.consume(']'); - return extend(function(self, locals) { + return extend(function $parseObjectIndex(self, locals) { var o = obj(self, locals), i = indexFn(self, locals), - v, p; + v; - ensureSafeMemberName(i, parser.text); + ensureSafeMemberName(i, expression); if (!o) return undefined; - v = ensureSafeObject(o[i], parser.text); - if (v && v.then && parser.options.unwrapPromises) { - p = v; - if (!('$$v' in v)) { - p.$$v = undefined; - p.then(function(val) { p.$$v = val; }); - } - v = v.$$v; - } + v = ensureSafeObject(o[i], expression); return v; }, { assign: function(self, value, locals) { - var key = indexFn(self, locals); + var key = ensureSafeMemberName(indexFn(self, locals), expression); // prevent overwriting of Function.constructor which would break ensureSafeObject check - var safe = ensureSafeObject(obj(self, locals), parser.text); - return safe[key] = value; + var o = ensureSafeObject(obj(self, locals), expression); + if (!o) obj.assign(self, o = {}); + return o[key] = value; } }); }, - functionCall: function(fn, contextGetter) { + functionCall: function(fnGetter, contextGetter) { var argsFn = []; if (this.peekToken().text !== ')') { do { @@ -10754,26 +11364,30 @@ Parser.prototype = { } this.consume(')'); - var parser = this; + var expressionText = this.text; + // we can safely reuse the array across invocations + var args = argsFn.length ? [] : null; - return function(scope, locals) { - var args = []; + return function $parseFunctionCall(scope, locals) { var context = contextGetter ? contextGetter(scope, locals) : scope; + var fn = fnGetter(scope, locals, context) || noop; - for (var i = 0; i < argsFn.length; i++) { - args.push(argsFn[i](scope, locals)); + if (args) { + var i = argsFn.length; + while (i--) { + args[i] = ensureSafeObject(argsFn[i](scope, locals), expressionText); + } } - var fnPtr = fn(scope, locals, context) || noop; - ensureSafeObject(context, parser.text); - ensureSafeFunction(fnPtr, parser.text); + ensureSafeObject(context, expressionText); + ensureSafeFunction(fn, expressionText); // IE stupidity! (IE doesn't have apply for some native functions) - var v = fnPtr.apply - ? fnPtr.apply(context, args) - : fnPtr(args[0], args[1], args[2], args[3], args[4]); + var v = fn.apply + ? fn.apply(context, args) + : fn(args[0], args[1], args[2], args[3], args[4]); - return ensureSafeObject(v, parser.text); + return ensureSafeObject(v, expressionText); }; }, @@ -10796,9 +11410,9 @@ Parser.prototype = { } this.consume(']'); - return extend(function(self, locals) { + return extend(function $parseArrayLiteral(self, locals) { var array = []; - for (var i = 0; i < elementFns.length; i++) { + for (var i = 0, ii = elementFns.length; i < ii; i++) { array.push(elementFns[i](self, locals)); } return array; @@ -10829,9 +11443,9 @@ Parser.prototype = { } this.consume('}'); - return extend(function(self, locals) { + return extend(function $parseObjectLiteral(self, locals) { var object = {}; - for (var i = 0; i < keyValues.length; i++) { + for (var i = 0, ii = keyValues.length; i < ii; i++) { var keyValue = keyValues[i]; object[keyValue.key] = keyValue.value(self, locals); } @@ -10848,173 +11462,83 @@ Parser.prototype = { // Parser helper functions ////////////////////////////////////////////////// -function setter(obj, path, setValue, fullExp, options) { - //needed? - options = options || {}; +function setter(obj, path, setValue, fullExp) { + ensureSafeObject(obj, fullExp); var element = path.split('.'), key; for (var i = 0; element.length > 1; i++) { key = ensureSafeMemberName(element.shift(), fullExp); - var propertyObj = obj[key]; + var propertyObj = ensureSafeObject(obj[key], fullExp); if (!propertyObj) { propertyObj = {}; obj[key] = propertyObj; } obj = propertyObj; - if (obj.then && options.unwrapPromises) { - promiseWarning(fullExp); - if (!("$$v" in obj)) { - (function(promise) { - promise.then(function(val) { promise.$$v = val; }); } - )(obj); - } - if (obj.$$v === undefined) { - obj.$$v = {}; - } - obj = obj.$$v; - } } key = ensureSafeMemberName(element.shift(), fullExp); - ensureSafeObject(obj, fullExp); ensureSafeObject(obj[key], fullExp); obj[key] = setValue; return setValue; } -var getterFnCache = {}; +var getterFnCache = createMap(); /** * Implementation of the "Black Hole" variant from: * - http://jsperf.com/angularjs-parse-getter/4 * - http://jsperf.com/path-evaluation-simplified/7 */ -function cspSafeGetterFn(key0, key1, key2, key3, key4, fullExp, options) { +function cspSafeGetterFn(key0, key1, key2, key3, key4, fullExp) { ensureSafeMemberName(key0, fullExp); ensureSafeMemberName(key1, fullExp); ensureSafeMemberName(key2, fullExp); ensureSafeMemberName(key3, fullExp); ensureSafeMemberName(key4, fullExp); - return !options.unwrapPromises - ? function cspSafeGetter(scope, locals) { - var pathVal = (locals && locals.hasOwnProperty(key0)) ? locals : scope; - - if (pathVal == null) return pathVal; - pathVal = pathVal[key0]; + return function cspSafeGetter(scope, locals) { + var pathVal = (locals && locals.hasOwnProperty(key0)) ? locals : scope; - if (!key1) return pathVal; - if (pathVal == null) return undefined; - pathVal = pathVal[key1]; + if (pathVal == null) return pathVal; + pathVal = pathVal[key0]; - if (!key2) return pathVal; - if (pathVal == null) return undefined; - pathVal = pathVal[key2]; + if (!key1) return pathVal; + if (pathVal == null) return undefined; + pathVal = pathVal[key1]; - if (!key3) return pathVal; - if (pathVal == null) return undefined; - pathVal = pathVal[key3]; + if (!key2) return pathVal; + if (pathVal == null) return undefined; + pathVal = pathVal[key2]; - if (!key4) return pathVal; - if (pathVal == null) return undefined; - pathVal = pathVal[key4]; - - return pathVal; - } - : function cspSafePromiseEnabledGetter(scope, locals) { - var pathVal = (locals && locals.hasOwnProperty(key0)) ? locals : scope, - promise; - - if (pathVal == null) return pathVal; - - pathVal = pathVal[key0]; - if (pathVal && pathVal.then) { - promiseWarning(fullExp); - if (!("$$v" in pathVal)) { - promise = pathVal; - promise.$$v = undefined; - promise.then(function(val) { promise.$$v = val; }); - } - pathVal = pathVal.$$v; - } + if (!key3) return pathVal; + if (pathVal == null) return undefined; + pathVal = pathVal[key3]; - if (!key1) return pathVal; - if (pathVal == null) return undefined; - pathVal = pathVal[key1]; - if (pathVal && pathVal.then) { - promiseWarning(fullExp); - if (!("$$v" in pathVal)) { - promise = pathVal; - promise.$$v = undefined; - promise.then(function(val) { promise.$$v = val; }); - } - pathVal = pathVal.$$v; - } + if (!key4) return pathVal; + if (pathVal == null) return undefined; + pathVal = pathVal[key4]; - if (!key2) return pathVal; - if (pathVal == null) return undefined; - pathVal = pathVal[key2]; - if (pathVal && pathVal.then) { - promiseWarning(fullExp); - if (!("$$v" in pathVal)) { - promise = pathVal; - promise.$$v = undefined; - promise.then(function(val) { promise.$$v = val; }); - } - pathVal = pathVal.$$v; - } - - if (!key3) return pathVal; - if (pathVal == null) return undefined; - pathVal = pathVal[key3]; - if (pathVal && pathVal.then) { - promiseWarning(fullExp); - if (!("$$v" in pathVal)) { - promise = pathVal; - promise.$$v = undefined; - promise.then(function(val) { promise.$$v = val; }); - } - pathVal = pathVal.$$v; - } - - if (!key4) return pathVal; - if (pathVal == null) return undefined; - pathVal = pathVal[key4]; - if (pathVal && pathVal.then) { - promiseWarning(fullExp); - if (!("$$v" in pathVal)) { - promise = pathVal; - promise.$$v = undefined; - promise.then(function(val) { promise.$$v = val; }); - } - pathVal = pathVal.$$v; - } - return pathVal; - }; + return pathVal; + }; } function getterFn(path, options, fullExp) { - // Check whether the cache has this getter already. - // We can use hasOwnProperty directly on the cache because we ensure, - // see below, that the cache never stores a path called 'hasOwnProperty' - if (getterFnCache.hasOwnProperty(path)) { - return getterFnCache[path]; - } + var fn = getterFnCache[path]; + + if (fn) return fn; var pathKeys = path.split('.'), - pathKeysLength = pathKeys.length, - fn; + pathKeysLength = pathKeys.length; // http://jsperf.com/angularjs-parse-getter/6 if (options.csp) { if (pathKeysLength < 6) { - fn = cspSafeGetterFn(pathKeys[0], pathKeys[1], pathKeys[2], pathKeys[3], pathKeys[4], fullExp, - options); + fn = cspSafeGetterFn(pathKeys[0], pathKeys[1], pathKeys[2], pathKeys[3], pathKeys[4], fullExp); } else { fn = function(scope, locals) { var i = 0, val; do { val = cspSafeGetterFn(pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++], - pathKeys[i++], fullExp, options)(scope, locals); + pathKeys[i++], fullExp)(scope, locals); locals = undefined; // clear after first iteration scope = val; @@ -11023,7 +11547,7 @@ function getterFn(path, options, fullExp) { }; } } else { - var code = 'var p;\n'; + var code = ''; forEach(pathKeys, function(key, index) { ensureSafeMemberName(key, fullExp); code += 'if(s == null) return undefined;\n' + @@ -11031,35 +11555,23 @@ function getterFn(path, options, fullExp) { // we simply dereference 's' on any .dot notation ? 's' // but if we are first then we check locals first, and if so read it first - : '((k&&k.hasOwnProperty("' + key + '"))?k:s)') + '["' + key + '"]' + ';\n' + - (options.unwrapPromises - ? 'if (s && s.then) {\n' + - ' pw("' + fullExp.replace(/(["\r\n])/g, '\\$1') + '");\n' + - ' if (!("$$v" in s)) {\n' + - ' p=s;\n' + - ' p.$$v = undefined;\n' + - ' p.then(function(v) {p.$$v=v;});\n' + - '}\n' + - ' s=s.$$v\n' + - '}\n' - : ''); + : '((l&&l.hasOwnProperty("' + key + '"))?l:s)') + '.' + key + ';\n'; }); code += 'return s;'; /* jshint -W054 */ - var evaledFnGetter = new Function('s', 'k', 'pw', code); // s=scope, k=locals, pw=promiseWarning + var evaledFnGetter = new Function('s', 'l', code); // s=scope, l=locals /* jshint +W054 */ evaledFnGetter.toString = valueFn(code); - fn = options.unwrapPromises ? function(scope, locals) { - return evaledFnGetter(scope, locals, promiseWarning); - } : evaledFnGetter; - } + evaledFnGetter.assign = function(self, value) { + return setter(self, path, value, path); + }; - // Only cache the value if it's not going to mess up the cache object - // This is more performant that using Object.prototype.hasOwnProperty.call - if (path !== 'hasOwnProperty') { - getterFnCache[path] = fn; + fn = evaledFnGetter; } + + fn.sharedGetter = true; + getterFnCache[path] = fn; return fn; } @@ -11109,142 +11621,146 @@ function getterFn(path, options, fullExp) { /** * @ngdoc provider * @name $parseProvider - * @kind function * * @description * `$parseProvider` can be used for configuring the default behavior of the {@link ng.$parse $parse} * service. */ function $ParseProvider() { - var cache = {}; + var cache = createMap(); var $parseOptions = { - csp: false, - unwrapPromises: false, - logPromiseWarnings: true - }; - - - /** - * @deprecated Promise unwrapping via $parse is deprecated and will be removed in the future. - * - * @ngdoc method - * @name $parseProvider#unwrapPromises - * @description - * - * **This feature is deprecated, see deprecation notes below for more info** - * - * If set to true (default is false), $parse will unwrap promises automatically when a promise is - * found at any part of the expression. In other words, if set to true, the expression will always - * result in a non-promise value. - * - * While the promise is unresolved, it's treated as undefined, but once resolved and fulfilled, - * the fulfillment value is used in place of the promise while evaluating the expression. - * - * **Deprecation notice** - * - * This is a feature that didn't prove to be wildly useful or popular, primarily because of the - * dichotomy between data access in templates (accessed as raw values) and controller code - * (accessed as promises). - * - * In most code we ended up resolving promises manually in controllers anyway and thus unifying - * the model access there. - * - * Other downsides of automatic promise unwrapping: - * - * - when building components it's often desirable to receive the raw promises - * - adds complexity and slows down expression evaluation - * - makes expression code pre-generation unattractive due to the amount of code that needs to be - * generated - * - makes IDE auto-completion and tool support hard - * - * **Warning Logs** - * - * If the unwrapping is enabled, Angular will log a warning about each expression that unwraps a - * promise (to reduce the noise, each expression is logged only once). To disable this logging use - * `$parseProvider.logPromiseWarnings(false)` api. - * - * - * @param {boolean=} value New value. - * @returns {boolean|self} Returns the current setting when used as getter and self if used as - * setter. - */ - this.unwrapPromises = function(value) { - if (isDefined(value)) { - $parseOptions.unwrapPromises = !!value; - return this; - } else { - return $parseOptions.unwrapPromises; - } + csp: false }; - /** - * @deprecated Promise unwrapping via $parse is deprecated and will be removed in the future. - * - * @ngdoc method - * @name $parseProvider#logPromiseWarnings - * @description - * - * Controls whether Angular should log a warning on any encounter of a promise in an expression. - * - * The default is set to `true`. - * - * This setting applies only if `$parseProvider.unwrapPromises` setting is set to true as well. - * - * @param {boolean=} value New value. - * @returns {boolean|self} Returns the current setting when used as getter and self if used as - * setter. - */ - this.logPromiseWarnings = function(value) { - if (isDefined(value)) { - $parseOptions.logPromiseWarnings = value; - return this; - } else { - return $parseOptions.logPromiseWarnings; - } - }; + this.$get = ['$filter', '$sniffer', function($filter, $sniffer) { + $parseOptions.csp = $sniffer.csp; + function wrapSharedExpression(exp) { + var wrapped = exp; - this.$get = ['$filter', '$sniffer', '$log', function($filter, $sniffer, $log) { - $parseOptions.csp = $sniffer.csp; + if (exp.sharedGetter) { + wrapped = function $parseWrapper(self, locals) { + return exp(self, locals); + }; + wrapped.literal = exp.literal; + wrapped.constant = exp.constant; + wrapped.assign = exp.assign; + } - promiseWarning = function promiseWarningFn(fullExp) { - if (!$parseOptions.logPromiseWarnings || promiseWarningCache.hasOwnProperty(fullExp)) return; - promiseWarningCache[fullExp] = true; - $log.warn('[$parse] Promise found in the expression `' + fullExp + '`. ' + - 'Automatic unwrapping of promises in Angular expressions is deprecated.'); - }; + return wrapped; + } - return function(exp) { - var parsedExpression; + return function $parse(exp, interceptorFn) { + var parsedExpression, oneTime, cacheKey; switch (typeof exp) { case 'string': + cacheKey = exp = exp.trim(); - if (cache.hasOwnProperty(exp)) { - return cache[exp]; - } + parsedExpression = cache[cacheKey]; + + if (!parsedExpression) { + if (exp.charAt(0) === ':' && exp.charAt(1) === ':') { + oneTime = true; + exp = exp.substring(2); + } - var lexer = new Lexer($parseOptions); - var parser = new Parser(lexer, $filter, $parseOptions); - parsedExpression = parser.parse(exp); + 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) { + //oneTime is not part of the exp passed to the Parser so we may have to + //wrap the parsedExpression before adding a $$watchDelegate + parsedExpression = wrapSharedExpression(parsedExpression); + parsedExpression.$$watchDelegate = parsedExpression.literal ? + oneTimeLiteralWatchDelegate : oneTimeWatchDelegate; + } - if (exp !== 'hasOwnProperty') { - // Only cache the value if it's not going to mess up the cache object - // This is more performant that using Object.prototype.hasOwnProperty.call - cache[exp] = parsedExpression; + cache[cacheKey] = parsedExpression; } - - return parsedExpression; + return addInterceptor(parsedExpression, interceptorFn); case 'function': - return exp; + return addInterceptor(exp, interceptorFn); default: - return noop; + return addInterceptor(noop, interceptorFn); } }; + + function oneTimeWatchDelegate(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.apply(this, arguments); + } + if (isDefined(value)) { + scope.$$postDigest(function () { + if (isDefined(lastValue)) { + unwatch(); + } + }); + } + }, objectEquality); + } + + function oneTimeLiteralWatchDelegate(scope, listener, objectEquality, parsedExpression) { + var unwatch; + return unwatch = scope.$watch(function oneTimeWatch(scope) { + return parsedExpression(scope); + }, function oneTimeListener(value, old, scope) { + if (isFunction(listener)) { + listener.call(this, value, old, scope); + } + if (isAllDefined(value)) { + scope.$$postDigest(function () { + if(isAllDefined(value)) unwatch(); + }); + } + }, objectEquality); + + 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) { + return parsedExpression(scope); + }, function constantListener(value, old, scope) { + if (isFunction(listener)) { + listener.apply(this, arguments); + } + unwatch(); + }, objectEquality); + } + + function addInterceptor(parsedExpression, interceptorFn) { + if (!interceptorFn) return parsedExpression; + + var fn = function interceptedExpression(scope, locals) { + var value = parsedExpression(scope, locals); + 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; + }; + fn.$$watchDelegate = parsedExpression.$$watchDelegate; + return fn; + } }]; } @@ -11256,6 +11772,46 @@ function $ParseProvider() { * @description * A promise/deferred implementation 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 promises to some degree. + * + * # $q constructor + * + * The streamlined ES6 style promise is essentially just using $q as a constructor which takes a `resolver` + * function as the first argument. This is similar to the native Promise implementation from ES6 Harmony, + * see [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise). + * + * While the constructor-style use is supported, not all of the supporting methods from ES6 Harmony promises are + * available yet. + * + * It can be used like so: + * + * ```js + * return $q(function(resolve, reject) { + * // perform some asynchronous operation, resolve or reject the promise when appropriate. + * setInterval(function() { + * if (pollStatus > 0) { + * resolve(polledValue); + * } else if (pollStatus < 0) { + * reject(polledValue); + * } else { + * pollStatus = pollAgain(function(value) { + * polledValue = value; + * }); + * } + * }, 10000); + * }). + * then(function(value) { + * // handle success + * }, function(reason) { + * // handle failure + * }); + * ``` + * + * Note: progress/notify callbacks are not currently supported via the ES6-style interface. + * + * However, the more traditional CommonJS-style usage is still available, and documented below. + * * [The CommonJS Promise proposal](http://wiki.commonjs.org/wiki/Promises) describes a promise as an * interface for interacting with an object that represents the result of an action that is * performed asynchronously, and may or may not be finished at any given point in time. @@ -11302,7 +11858,6 @@ function $ParseProvider() { * For more on this please see the [Q documentation](https://github.com/kriskowal/q) especially the * section on serial or parallel joining of promises. * - * * # The Deferred API * * A new instance of deferred is constructed by calling `$q.defer()`. @@ -11343,7 +11898,7 @@ function $ParseProvider() { * * This method *returns a new promise* which is resolved or rejected via the return value of the * `successCallback`, `errorCallback`. It also notifies via the return value of the - * `notifyCallback` method. The promise can not be resolved or rejected from the notifyCallback + * `notifyCallback` method. The promise cannot be resolved or rejected from the notifyCallback * method. * * - `catch(errorCallback)` – shorthand for `promise.then(null, errorCallback)` @@ -11411,6 +11966,12 @@ function $ParseProvider() { * expect(resolvedValue).toEqual(123); * })); * ``` + * + * @param {function(function, function)} resolver Function which is responsible for resolving or + * rejecting the newly created promise. The first parameter is a function which resolves the + * promise, the second parameter is a function which rejects the promise. + * + * @returns {Promise} The newly created promise. */ function $QProvider() { @@ -11421,20 +11982,40 @@ function $QProvider() { }]; } +function $$QProvider() { + this.$get = ['$browser', '$exceptionHandler', function($browser, $exceptionHandler) { + return qFactory(function(callback) { + $browser.defer(callback); + }, $exceptionHandler); + }]; +} /** * Constructs a promise manager. * - * @param {function(Function)} nextTick Function for executing functions in the next turn. + * @param {function(function)} nextTick Function for executing functions in the next turn. * @param {function(...*)} exceptionHandler Function into which unexpected exceptions are passed for * debugging purposes. * @returns {object} Promise manager. */ function qFactory(nextTick, exceptionHandler) { + var $qMinErr = minErr('$q', TypeError); + function callOnce(self, resolveFn, rejectFn) { + var called = false; + function wrap(fn) { + return function(value) { + if (called) return; + called = true; + fn.call(self, value); + }; + } + + return [wrap(resolveFn), wrap(rejectFn)]; + } /** * @ngdoc method - * @name $q#defer + * @name ng.$q#defer * @kind function * * @description @@ -11443,152 +12024,148 @@ function qFactory(nextTick, exceptionHandler) { * @returns {Deferred} Returns a new instance of deferred. */ var defer = function() { - var pending = [], - value, deferred; - - deferred = { - - resolve: function(val) { - if (pending) { - var callbacks = pending; - pending = undefined; - value = ref(val); - - if (callbacks.length) { - nextTick(function() { - var callback; - for (var i = 0, ii = callbacks.length; i < ii; i++) { - callback = callbacks[i]; - value.then(callback[0], callback[1], callback[2]); - } - }); - } - } - }, - - - reject: function(reason) { - deferred.resolve(createInternalRejectedPromise(reason)); - }, + return new Deferred(); + }; + function Promise() { + this.$$state = { status: 0 }; + } - notify: function(progress) { - if (pending) { - var callbacks = pending; + Promise.prototype = { + then: function(onFulfilled, onRejected, progressBack) { + var result = new Deferred(); - if (pending.length) { - nextTick(function() { - var callback; - for (var i = 0, ii = callbacks.length; i < ii; i++) { - callback = callbacks[i]; - callback[2](progress); - } - }); - } - } - }, + 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; + }, - promise: { - then: function(callback, errback, progressback) { - var result = defer(); + "catch": function(callback) { + return this.then(null, callback); + }, - var wrappedCallback = function(value) { - try { - result.resolve((isFunction(callback) ? callback : defaultCallback)(value)); - } catch(e) { - result.reject(e); - exceptionHandler(e); - } - }; + "finally": function(callback, progressBack) { + return this.then(function(value) { + return handleCallback(value, true, callback); + }, function(error) { + return handleCallback(error, false, callback); + }, progressBack); + } + }; - var wrappedErrback = function(reason) { - try { - result.resolve((isFunction(errback) ? errback : defaultErrback)(reason)); - } catch(e) { - result.reject(e); - exceptionHandler(e); - } - }; + //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); + }; + } - var wrappedProgressback = function(progress) { - try { - result.notify((isFunction(progressback) ? progressback : defaultCallback)(progress)); - } catch(e) { - exceptionHandler(e); - } - }; + function processQueue(state) { + var fn, promise, pending; - if (pending) { - pending.push([wrappedCallback, wrappedErrback, wrappedProgressback]); - } else { - value.then(wrappedCallback, wrappedErrback, wrappedProgressback); - } + pending = state.pending; + state.processScheduled = false; + state.pending = undefined; + for (var i = 0, ii = pending.length; i < ii; ++i) { + promise = pending[i][0]; + fn = pending[i][state.status]; + try { + if (isFunction(fn)) { + promise.resolve(fn(state.value)); + } else if (state.status === 1) { + promise.resolve(state.value); + } else { + promise.reject(state.value); + } + } catch(e) { + promise.reject(e); + exceptionHandler(e); + } + } + } - return result.promise; - }, + function scheduleProcessQueue(state) { + if (state.processScheduled || !state.pending) return; + state.processScheduled = true; + nextTick(function() { processQueue(state); }); + } - "catch": function(callback) { - return this.then(null, callback); - }, + function Deferred() { + this.promise = new Promise(); + //Necessary to support unbound execution :/ + this.resolve = simpleBind(this, this.resolve); + this.reject = simpleBind(this, this.reject); + this.notify = simpleBind(this, this.notify); + } - "finally": function(callback) { + Deferred.prototype = { + resolve: function(val) { + if (this.promise.$$state.status) return; + if (val === this.promise) { + this.$$reject($qMinErr( + 'qcycle', + "Expected promise to be resolved with value other than itself '{0}'", + val)); + } + else { + this.$$resolve(val); + } - function makePromise(value, resolved) { - var result = defer(); - if (resolved) { - result.resolve(value); - } else { - result.reject(value); - } - return result.promise; - } + }, - function handleCallback(value, isResolved) { - var callbackOutput = null; - try { - callbackOutput = (callback ||defaultCallback)(); - } catch(e) { - return makePromise(e, false); - } - if (callbackOutput && isFunction(callbackOutput.then)) { - return callbackOutput.then(function() { - return makePromise(value, isResolved); - }, function(error) { - return makePromise(error, false); - }); - } else { - return makePromise(value, isResolved); - } - } + $$resolve: function(val) { + var then, fns; - return this.then(function(value) { - return handleCallback(value, true); - }, function(error) { - return handleCallback(error, false); - }); + fns = callOnce(this, this.$$resolve, this.$$reject); + try { + if ((isObject(val) || isFunction(val))) then = val && val.then; + if (isFunction(then)) { + this.promise.$$state.status = -1; + then.call(val, fns[0], fns[1], this.notify); + } else { + this.promise.$$state.value = val; + this.promise.$$state.status = 1; + scheduleProcessQueue(this.promise.$$state); } + } catch(e) { + fns[1](e); + exceptionHandler(e); } - }; + }, - return deferred; - }; + reject: function(reason) { + if (this.promise.$$state.status) return; + this.$$reject(reason); + }, + + $$reject: function(reason) { + this.promise.$$state.value = reason; + this.promise.$$state.status = 2; + scheduleProcessQueue(this.promise.$$state); + }, + notify: function(progress) { + var callbacks = this.promise.$$state.pending; - var ref = function(value) { - if (value && isFunction(value.then)) return value; - return { - then: function(callback) { - var result = defer(); + if ((this.promise.$$state.status <= 0) && callbacks && callbacks.length) { nextTick(function() { - result.resolve(callback(value)); + 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); + } + } }); - return result.promise; } - }; + } }; - /** * @ngdoc method * @name $q#reject @@ -11626,28 +12203,38 @@ function qFactory(nextTick, exceptionHandler) { * @returns {Promise} Returns a promise that was already resolved as rejected with the `reason`. */ var reject = function(reason) { - var result = defer(); + var result = new Deferred(); result.reject(reason); return result.promise; }; - var createInternalRejectedPromise = function(reason) { - return { - then: function(callback, errback) { - var result = defer(); - nextTick(function() { - try { - result.resolve((isFunction(errback) ? errback : defaultErrback)(reason)); - } catch(e) { - result.reject(e); - exceptionHandler(e); - } - }); - return result.promise; - } - }; + var makePromise = function makePromise(value, resolved) { + var result = new Deferred(); + if (resolved) { + result.resolve(value); + } else { + result.reject(value); + } + return result.promise; }; + var handleCallback = function handleCallback(value, isResolved, callback) { + var callbackOutput = null; + try { + if (isFunction(callback)) callbackOutput = callback(); + } catch(e) { + return makePromise(e, false); + } + if (isPromiseLike(callbackOutput)) { + return callbackOutput.then(function() { + return makePromise(value, isResolved); + }, function(error) { + return makePromise(error, false); + }); + } else { + return makePromise(value, isResolved); + } + }; /** * @ngdoc method @@ -11662,65 +12249,14 @@ function qFactory(nextTick, exceptionHandler) { * @param {*} value Value or a promise * @returns {Promise} Returns a promise of the passed value or promise */ - var when = function(value, callback, errback, progressback) { - var result = defer(), - done; - var wrappedCallback = function(value) { - try { - return (isFunction(callback) ? callback : defaultCallback)(value); - } catch (e) { - exceptionHandler(e); - return reject(e); - } - }; - - var wrappedErrback = function(reason) { - try { - return (isFunction(errback) ? errback : defaultErrback)(reason); - } catch (e) { - exceptionHandler(e); - return reject(e); - } - }; - - var wrappedProgressback = function(progress) { - try { - return (isFunction(progressback) ? progressback : defaultCallback)(progress); - } catch (e) { - exceptionHandler(e); - } - }; - - nextTick(function() { - ref(value).then(function(value) { - if (done) return; - done = true; - result.resolve(ref(value).then(wrappedCallback, wrappedErrback, wrappedProgressback)); - }, function(reason) { - if (done) return; - done = true; - result.resolve(wrappedErrback(reason)); - }, function(progress) { - if (done) return; - result.notify(wrappedProgressback(progress)); - }); - }); - return result.promise; + var when = function(value, callback, errback, progressBack) { + var result = new Deferred(); + result.resolve(value); + return result.promise.then(callback, errback, progressBack); }; - - function defaultCallback(value) { - return value; - } - - - function defaultErrback(reason) { - return reject(reason); - } - - /** * @ngdoc method * @name $q#all @@ -11736,14 +12272,15 @@ function qFactory(nextTick, exceptionHandler) { * If any of the promises is resolved with a rejection, this resulting promise will be rejected * with the same rejection value. */ + function all(promises) { - var deferred = defer(), + var deferred = new Deferred(), counter = 0, results = isArray(promises) ? [] : {}; forEach(promises, function(promise, key) { counter++; - ref(promise).then(function(value) { + when(promise).then(function(value) { if (results.hasOwnProperty(key)) return; results[key] = value; if (!(--counter)) deferred.resolve(results); @@ -11760,12 +12297,37 @@ function qFactory(nextTick, exceptionHandler) { return deferred.promise; } - return { - defer: defer, - reject: reject, - when: when, - all: all + var $Q = function Q(resolver) { + if (!isFunction(resolver)) { + throw $qMinErr('norslvr', "Expected resolverFn, got '{0}'", resolver); + } + + if (!(this instanceof Q)) { + // More useful when $Q is the Promise itself. + return new Q(resolver); + } + + var deferred = new Deferred(); + + function resolveFn(value) { + deferred.resolve(value); + } + + function rejectFn(reason) { + deferred.reject(reason); + } + + resolver(resolveFn, rejectFn); + + return deferred.promise; }; + + $Q.defer = defer; + $Q.reject = reject; + $Q.when = when; + $Q.all = all; + + return $Q; } function $$RAFProvider(){ //rAF @@ -11871,6 +12433,7 @@ function $RootScopeProvider(){ var TTL = 10; var $rootScopeMinErr = minErr('$rootScope'); var lastDirtyWatch = null; + var applyAsyncId = null; this.digestTtl = function(value) { if (arguments.length) { @@ -11934,15 +12497,32 @@ function $RootScopeProvider(){ this.$$listeners = {}; this.$$listenerCount = {}; this.$$isolateBindings = {}; + this.$$applyAsyncQueue = []; } /** * @ngdoc property * @name $rootScope.Scope#$id - * @returns {number} Unique scope ID (monotonically increasing alphanumeric sequence) useful for - * debugging. + * + * @description + * Unique scope ID (monotonically increasing) useful for debugging. */ + /** + * @ngdoc property + * @name $rootScope.Scope#$parent + * + * @description + * Reference to the parent scope. + */ + + /** + * @ngdoc property + * @name $rootScope.Scope#$root + * + * @description + * Reference to the root scope. + */ Scope.prototype = { constructor: Scope, @@ -11954,9 +12534,8 @@ function $RootScopeProvider(){ * @description * Creates a new child {@link ng.$rootScope.Scope scope}. * - * The parent scope will propagate the {@link ng.$rootScope.Scope#$digest $digest()} and - * {@link ng.$rootScope.Scope#$digest $digest()} events. The scope can be removed from the - * scope hierarchy using {@link ng.$rootScope.Scope#$destroy $destroy()}. + * The parent scope will propagate the {@link ng.$rootScope.Scope#$digest $digest()} event. + * The scope can be removed from the scope hierarchy using {@link ng.$rootScope.Scope#$destroy $destroy()}. * * {@link ng.$rootScope.Scope#$destroy $destroy()} must be called on a scope when it is * desired for the scope and its child scopes to be permanently detached from the parent and @@ -11971,8 +12550,7 @@ function $RootScopeProvider(){ * */ $new: function(isolate) { - var ChildScope, - child; + var child; if (isolate) { child = new Scope(); @@ -11983,18 +12561,18 @@ function $RootScopeProvider(){ } else { // Only create a child scope class if somebody asks for one, // but cache it to allow the VM to optimize lookups. - if (!this.$$childScopeClass) { - this.$$childScopeClass = function() { + if (!this.$$ChildScope) { + this.$$ChildScope = function ChildScope() { this.$$watchers = this.$$nextSibling = this.$$childHead = this.$$childTail = null; this.$$listeners = {}; this.$$listenerCount = {}; this.$id = nextUid(); - this.$$childScopeClass = null; + this.$$ChildScope = null; }; - this.$$childScopeClass.prototype = this; + this.$$ChildScope.prototype = this; } - child = new this.$$childScopeClass(); + child = new this.$$ChildScope(); } child['this'] = child; child.$parent = this; @@ -12048,7 +12626,6 @@ function $RootScopeProvider(){ * can compare the `newVal` and `oldVal`. If these two values are identical (`===`) then the * listener was called due to initialization. * - * The example below contains an illustration of using a function as your $watch listener * * * # Example @@ -12078,14 +12655,14 @@ function $RootScopeProvider(){ - // Using a listener function + // Using a function as a watchExpression var food; scope.foodCounter = 0; expect(scope.foodCounter).toEqual(0); scope.$watch( - // This is the listener function + // This function returns the value being watched. It is called for each turn of the $digest loop function() { return food; }, - // This is the change handler + // This is the change listener, called when the value returned from the above function changes function(newValue, oldValue) { if ( newValue !== oldValue ) { // Only increment the counter if the value changed @@ -12115,20 +12692,23 @@ function $RootScopeProvider(){ * * - `string`: Evaluated as {@link guide/expression expression} * - `function(scope)`: called with current `scope` as a parameter. - * @param {(function()|string)=} listener Callback called whenever the return value of - * the `watchExpression` changes. - * - * - `string`: Evaluated as {@link guide/expression expression} - * - `function(newValue, oldValue, scope)`: called with current and previous values as - * parameters. + * @param {function(newVal, oldVal, scope)} listener Callback called whenever the value + * of `watchExpression` changes. * + * - `newVal` contains the current value of the `watchExpression` + * - `oldVal` contains the previous value of the `watchExpression` + * - `scope` refers to the current scope * @param {boolean=} objectEquality Compare for object equality using {@link angular.equals} instead of * comparing for reference equality. * @returns {function()} Returns a deregistration function for this listener. */ $watch: function(watchExp, listener, objectEquality) { + var get = $parse(watchExp); + + if (get.$$watchDelegate) { + return get.$$watchDelegate(this, listener, objectEquality, get); + } var scope = this, - get = compileToFn(watchExp, 'watch'), array = scope.$$watchers, watcher = { fn: listener, @@ -12140,18 +12720,8 @@ function $RootScopeProvider(){ lastDirtyWatch = null; - // in the case user pass string, we need to compile it, do we really need this ? if (!isFunction(listener)) { - var listenFn = compileToFn(listener || noop, 'listener'); - watcher.fn = function(newVal, oldVal, scope) {listenFn(scope);}; - } - - if (typeof watchExp == 'string' && get.constant) { - var originalFn = watcher.fn; - watcher.fn = function(newVal, oldVal, scope) { - originalFn.call(this, newVal, oldVal, scope); - arrayRemove(array, watcher); - }; + watcher.fn = noop; } if (!array) { @@ -12167,6 +12737,89 @@ function $RootScopeProvider(){ }; }, + /** + * @ngdoc method + * @name $rootScope.Scope#$watchGroup + * @kind function + * + * @description + * A variant of {@link ng.$rootScope.Scope#$watch $watch()} where it watches an array of `watchExpressions`. + * If any one expression in the collection changes the `listener` is executed. + * + * - The items in the `watchExpressions` array are observed via standard $watch operation and are examined on every + * call to $digest() to see if any items changes. + * - 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 + * watched using {@link ng.$rootScope.Scope#$watch $watch()} + * + * @param {function(newValues, oldValues, scope)} listener Callback called whenever the return value of any + * expression in `watchExpressions` changes + * The `newValues` array contains the current values of the `watchExpressions`, with the indexes matching + * those of `watchExpression` + * and the `oldValues` array contains the previous values of the `watchExpressions`, with the indexes matching + * those of `watchExpression` + * The `scope` refers to the current scope. + * @returns {function()} Returns a de-registration function for all listeners. + */ + $watchGroup: function(watchExpressions, listener) { + var oldValues = new Array(watchExpressions.length); + var newValues = new Array(watchExpressions.length); + var deregisterFns = []; + var self = this; + var changeReactionScheduled = false; + var firstRun = true; + + if (!watchExpressions.length) { + // No expressions means we call the listener ASAP + var shouldCall = true; + self.$evalAsync(function () { + if (shouldCall) listener(newValues, newValues, self); + }); + return function deregisterWatchGroup() { + shouldCall = false; + }; + } + + if (watchExpressions.length === 1) { + // Special case size of one + return this.$watch(watchExpressions[0], function watchGroupAction(value, oldValue, scope) { + newValues[0] = value; + oldValues[0] = oldValue; + listener(newValues, (value === oldValue) ? newValues : oldValues, scope); + }); + } + + forEach(watchExpressions, function (expr, i) { + var unwatchFn = self.$watch(expr, function watchGroupSubAction(value, oldValue) { + newValues[i] = value; + oldValues[i] = oldValue; + if (!changeReactionScheduled) { + changeReactionScheduled = true; + self.$evalAsync(watchGroupAction); + } + }); + deregisterFns.push(unwatchFn); + }); + + function watchGroupAction() { + changeReactionScheduled = false; + + if (firstRun) { + firstRun = false; + listener(newValues, newValues, self); + } else { + listener(newValues, oldValues, self); + } + } + + return function deregisterWatchGroup() { + while (deregisterFns.length) { + deregisterFns.shift()(); + } + }; + }, + /** * @ngdoc method @@ -12235,15 +12888,15 @@ function $RootScopeProvider(){ // only track veryOldValue if the listener is asking for it var trackVeryOldValue = (listener.length > 1); var changeDetected = 0; - var objGetter = $parse(obj); + var changeDetector = $parse(obj, $watchCollectionInterceptor); var internalArray = []; var internalObject = {}; var initRun = true; var oldLength = 0; - function $watchCollectionWatch() { - newValue = objGetter(self); - var newLength, key; + function $watchCollectionInterceptor(_value) { + newValue = _value; + var newLength, key, bothNaN, newItem, oldItem; if (!isObject(newValue)) { // if primitive if (oldValue !== newValue) { @@ -12267,11 +12920,13 @@ function $RootScopeProvider(){ } // copy the items to oldValue and look for changes. for (var i = 0; i < newLength; i++) { - var bothNaN = (oldValue[i] !== oldValue[i]) && - (newValue[i] !== newValue[i]); - if (!bothNaN && (oldValue[i] !== newValue[i])) { + oldItem = oldValue[i]; + newItem = newValue[i]; + + bothNaN = (oldItem !== oldItem) && (newItem !== newItem); + if (!bothNaN && (oldItem !== newItem)) { changeDetected++; - oldValue[i] = newValue[i]; + oldValue[i] = newItem; } } } else { @@ -12286,14 +12941,18 @@ function $RootScopeProvider(){ for (key in newValue) { if (newValue.hasOwnProperty(key)) { newLength++; - if (oldValue.hasOwnProperty(key)) { - if (oldValue[key] !== newValue[key]) { + newItem = newValue[key]; + oldItem = oldValue[key]; + + if (key in oldValue) { + bothNaN = (oldItem !== oldItem) && (newItem !== newItem); + if (!bothNaN && (oldItem !== newItem)) { changeDetected++; - oldValue[key] = newValue[key]; + oldValue[key] = newItem; } } else { oldLength++; - oldValue[key] = newValue[key]; + oldValue[key] = newItem; changeDetected++; } } @@ -12302,7 +12961,7 @@ function $RootScopeProvider(){ // we used to have more keys, need to find them and destroy them. changeDetected++; for(key in oldValue) { - if (oldValue.hasOwnProperty(key) && !newValue.hasOwnProperty(key)) { + if (!newValue.hasOwnProperty(key)) { oldLength--; delete oldValue[key]; } @@ -12341,7 +13000,7 @@ function $RootScopeProvider(){ } } - return this.$watch($watchCollectionWatch, $watchCollectionAction); + return this.$watch(changeDetector, $watchCollectionAction); }, /** @@ -12361,7 +13020,7 @@ function $RootScopeProvider(){ * {@link ng.directive:ngController controllers} or in * {@link ng.$compileProvider#directive directives}. * Instead, you should call {@link ng.$rootScope.Scope#$apply $apply()} (typically from within - * a {@link ng.$compileProvider#directive directives}), which will force a `$digest()`. + * a {@link ng.$compileProvider#directive directive}), which will force a `$digest()`. * * If you want to be notified whenever `$digest()` is called, * you can register a `watchExpression` function with @@ -12407,6 +13066,15 @@ function $RootScopeProvider(){ logIdx, logMsg, asyncTask; beginPhase('$digest'); + // Check for changes to browser url that happened in sync before the call to $digest + $browser.$$checkUrlChange(); + + if (this === $rootScope && applyAsyncId !== null) { + // If this is the root scope, and $applyAsync has scheduled a deferred $apply(), then + // cancel the scheduled $apply and flush the queue of expressions to be evaluated. + $browser.defer.cancel(applyAsyncId); + flushApplyAsync(); + } lastDirtyWatch = null; @@ -12419,7 +13087,6 @@ function $RootScopeProvider(){ asyncTask = asyncQueue.shift(); asyncTask.scope.$eval(asyncTask.expression); } catch (e) { - clearPhase(); $exceptionHandler(e); } lastDirtyWatch = null; @@ -12462,7 +13129,6 @@ function $RootScopeProvider(){ } } } catch (e) { - clearPhase(); $exceptionHandler(e); } } @@ -12546,7 +13212,9 @@ function $RootScopeProvider(){ this.$$destroyed = true; if (this === $rootScope) return; - forEach(this.$$listenerCount, bind(null, decrementListenerCount, this)); + for (var eventName in this.$$listenerCount) { + decrementListenerCount(this, this.$$listenerCount[eventName], eventName); + } // sever all the references to parent scopes (after this cleanup, the current scope should // not be retained by any of our references and should be eligible for garbage collection) @@ -12573,7 +13241,7 @@ function $RootScopeProvider(){ // prevent NPEs since these methods have references to properties we nulled out this.$destroy = this.$digest = this.$apply = noop; - this.$on = this.$watch = function() { return noop; }; + this.$on = this.$watch = this.$watchGroup = function() { return noop; }; }, /** @@ -12719,6 +13387,33 @@ function $RootScopeProvider(){ /** * @ngdoc method + * @name $rootScope.Scope#$applyAsync + * @kind function + * + * @description + * Schedule the invokation of $apply to occur at a later time. The actual time difference + * varies across browsers, but is typically around ~10 milliseconds. + * + * This can be used to queue up multiple expressions which need to be evaluated in the same + * digest. + * + * @param {(string|function())=} exp An angular expression to be executed. + * + * - `string`: execute using the rules as defined in {@link guide/expression expression}. + * - `function(scope)`: execute the function with current `scope` parameter. + */ + $applyAsync: function(expr) { + var scope = this; + expr && $rootScope.$$applyAsyncQueue.push($applyAsyncExpression); + scheduleApplyAsync(); + + function $applyAsyncExpression() { + scope.$eval(expr); + } + }, + + /** + * @ngdoc method * @name $rootScope.Scope#$on * @kind function * @@ -12731,7 +13426,8 @@ function $RootScopeProvider(){ * * - `targetScope` - `{Scope}`: the scope on which the event was `$emit`-ed or * `$broadcast`-ed. - * - `currentScope` - `{Scope}`: the current scope which is handling the event. + * - `currentScope` - `{Scope}`: the scope that is currently handling the event. Once the + * event propagates through the scope hierarchy, this property is set to null. * - `name` - `{string}`: name of the event. * - `stopPropagation` - `{function=}`: calling `stopPropagation` function will cancel * further event propagation (available only for events that were `$emit`-ed). @@ -12760,7 +13456,7 @@ function $RootScopeProvider(){ var self = this; return function() { - namedListeners[indexOf(namedListeners, listener)] = null; + namedListeners[namedListeners.indexOf(listener)] = null; decrementListenerCount(self, 1, name); }; }, @@ -12825,11 +13521,16 @@ function $RootScopeProvider(){ } } //if any listener on the current scope stops propagation, prevent bubbling - if (stopPropagation) return event; + if (stopPropagation) { + event.currentScope = null; + return event; + } //traverse upwards scope = scope.$parent; } while (scope); + event.currentScope = null; + return event; }, @@ -12866,8 +13567,11 @@ function $RootScopeProvider(){ event.defaultPrevented = true; }, defaultPrevented: false - }, - listenerArgs = concat([event], arguments, 1), + }; + + if (!target.$$listenerCount[name]) return event; + + var listenerArgs = concat([event], arguments, 1), listeners, i, length; //down while you can, then up and next sibling or up and next sibling until back at root @@ -12902,6 +13606,7 @@ function $RootScopeProvider(){ } } + event.currentScope = null; return event; } }; @@ -12923,11 +13628,6 @@ function $RootScopeProvider(){ $rootScope.$$phase = null; } - function compileToFn(exp, name) { - var fn = $parse(exp); - assertArgFn(fn, name); - return fn; - } function decrementListenerCount(current, count, name) { do { @@ -12944,6 +13644,26 @@ function $RootScopeProvider(){ * because it's unique we can easily tell it apart from other values */ function initWatchVal() {} + + function flushApplyAsync() { + var queue = $rootScope.$$applyAsyncQueue; + while (queue.length) { + try { + queue.shift()(); + } catch(e) { + $exceptionHandler(e); + } + } + applyAsyncId = null; + } + + function scheduleApplyAsync() { + if (applyAsyncId === null) { + applyAsyncId = $browser.defer(function() { + $rootScope.$apply(flushApplyAsync); + }); + } + } }]; } @@ -12953,7 +13673,7 @@ function $RootScopeProvider(){ */ function $$SanitizeUriProvider() { var aHrefSanitizationWhitelist = /^\s*(https?|ftp|mailto|tel|file):/, - imgSrcSanitizationWhitelist = /^\s*(https?|ftp|file):|data:image\//; + imgSrcSanitizationWhitelist = /^\s*((https?|ftp|file|blob):|data:image\/)/; /** * @description @@ -13514,7 +14234,7 @@ function $SceDelegateProvider() { * won't work on all browsers. Also, loading templates from `file://` URL does not work on some * browsers. * - * ## This feels like too much overhead for the developer? + * ## This feels like too much overhead * * It's important to remember that SCE only applies to interpolation expressions. * @@ -13598,7 +14318,7 @@ function $SceDelegateProvider() { * * <example module="mySceApp" deps="angular-sanitize.js"> * <file name="index.html"> - * <div ng-controller="myAppController as myCtrl"> + * <div ng-controller="AppController as myCtrl"> * <i ng-bind-html="myCtrl.explicitlyTrustedHtml" id="explicitlyTrustedHtml"></i><br><br> * <b>User comments</b><br> * By default, HTML that isn't explicitly trusted (e.g. Alice's comment) is sanitized when @@ -13615,17 +14335,17 @@ function $SceDelegateProvider() { * </file> * * <file name="script.js"> - * var mySceApp = angular.module('mySceApp', ['ngSanitize']); - * - * mySceApp.controller("myAppController", function myAppController($http, $templateCache, $sce) { - * var self = this; - * $http.get("test_data.json", {cache: $templateCache}).success(function(userComments) { - * self.userComments = userComments; - * }); - * self.explicitlyTrustedHtml = $sce.trustAsHtml( - * '<span onmouseover="this.textContent="Explicitly trusted HTML bypasses ' + - * 'sanitization."">Hover over this text.</span>'); - * }); + * angular.module('mySceApp', ['ngSanitize']) + * .controller('AppController', ['$http', '$templateCache', '$sce', + * function($http, $templateCache, $sce) { + * var self = this; + * $http.get("test_data.json", {cache: $templateCache}).success(function(userComments) { + * self.userComments = userComments; + * }); + * self.explicitlyTrustedHtml = $sce.trustAsHtml( + * '<span onmouseover="this.textContent="Explicitly trusted HTML bypasses ' + + * 'sanitization."">Hover over this text.</span>'); + * }]); * </file> * * <file name="test_data.json"> @@ -13807,9 +14527,9 @@ function $SceProvider() { if (parsed.literal && parsed.constant) { return parsed; } else { - return function sceParseAsTrusted(self, locals) { - return sce.getTrusted(type, parsed(self, locals)); - }; + return $parse(expr, function (value) { + return sce.getTrusted(type, value); + }); } }; @@ -14169,9 +14889,176 @@ function $SnifferProvider() { }]; } +var $compileMinErr = minErr('$compile'); + +/** + * @ngdoc service + * @name $templateRequest + * + * @description + * The `$templateRequest` service downloads the provided template using `$http` and, upon success, + * stores the contents inside of `$templateCache`. If the HTTP request fails or the response data + * of the HTTP request is empty then a `$compile` error will be thrown (the exception can be thwarted + * by setting the 2nd parameter of the function to true). + * + * @param {string} tpl The HTTP request template URL + * @param {boolean=} ignoreRequestError Whether or not to ignore the exception when the request fails or the template is empty + * + * @return {Promise} the HTTP Promise for the given. + * + * @property {number} totalPendingRequests total amount of pending template requests being downloaded. + */ +function $TemplateRequestProvider() { + this.$get = ['$templateCache', '$http', '$q', function($templateCache, $http, $q) { + function handleRequestFn(tpl, ignoreRequestError) { + var self = handleRequestFn; + self.totalPendingRequests++; + + return $http.get(tpl, { cache : $templateCache }) + .then(function(response) { + var html = response.data; + if(!html || html.length === 0) { + return handleError(); + } + + self.totalPendingRequests--; + $templateCache.put(tpl, html); + return html; + }, handleError); + + function handleError() { + self.totalPendingRequests--; + if (!ignoreRequestError) { + throw $compileMinErr('tpload', 'Failed to load template: {0}', tpl); + } + return $q.reject(); + } + } + + handleRequestFn.totalPendingRequests = 0; + + return handleRequestFn; + }]; +} + +function $$TestabilityProvider() { + this.$get = ['$rootScope', '$browser', '$location', + function($rootScope, $browser, $location) { + + /** + * @name $testability + * + * @description + * The private $$testability service provides a collection of methods for use when debugging + * or by automated test and debugging tools. + */ + var testability = {}; + + /** + * @name $$testability#findBindings + * + * @description + * Returns an array of elements that are bound (via ng-bind or {{}}) + * to expressions matching the input. + * + * @param {Element} element The element root to search from. + * @param {string} expression The binding expression to match. + * @param {boolean} opt_exactMatch If true, only returns exact matches + * for the expression. Filters and whitespace are ignored. + */ + testability.findBindings = function(element, expression, opt_exactMatch) { + var bindings = element.getElementsByClassName('ng-binding'); + var matches = []; + forEach(bindings, function(binding) { + var dataBinding = angular.element(binding).data('$binding'); + if (dataBinding) { + forEach(dataBinding, function(bindingName) { + if (opt_exactMatch) { + var matcher = new RegExp('(^|\\s)' + expression + '(\\s|\\||$)'); + if (matcher.test(bindingName)) { + matches.push(binding); + } + } else { + if (bindingName.indexOf(expression) != -1) { + matches.push(binding); + } + } + }); + } + }); + return matches; + }; + + /** + * @name $$testability#findModels + * + * @description + * Returns an array of elements that are two-way found via ng-model to + * expressions matching the input. + * + * @param {Element} element The element root to search from. + * @param {string} expression The model expression to match. + * @param {boolean} opt_exactMatch If true, only returns exact matches + * for the expression. + */ + testability.findModels = function(element, expression, opt_exactMatch) { + var prefixes = ['ng-', 'data-ng-', 'ng\\:']; + for (var p = 0; p < prefixes.length; ++p) { + var attributeEquals = opt_exactMatch ? '=' : '*='; + var selector = '[' + prefixes[p] + 'model' + attributeEquals + '"' + expression + '"]'; + var elements = element.querySelectorAll(selector); + if (elements.length) { + return elements; + } + } + }; + + /** + * @name $$testability#getLocation + * + * @description + * Shortcut for getting the location in a browser agnostic way. Returns + * the path, search, and hash. (e.g. /path?a=b#hash) + */ + testability.getLocation = function() { + return $location.url(); + }; + + /** + * @name $$testability#setLocation + * + * @description + * Shortcut for navigating to a location without doing a full page reload. + * + * @param {string} url The location url (path, search and hash, + * e.g. /path?a=b#hash) to go to. + */ + testability.setLocation = function(url) { + if (url !== $location.url()) { + $location.url(url); + $rootScope.$digest(); + } + }; + + /** + * @name $$testability#whenStable + * + * @description + * Calls the callback when $timeout and $http requests are completed. + * + * @param {function} callback + */ + testability.whenStable = function(callback) { + $browser.notifyWhenNoOutstandingRequests(callback); + }; + + return testability; + }]; +} + function $TimeoutProvider() { - this.$get = ['$rootScope', '$browser', '$q', '$exceptionHandler', - function($rootScope, $browser, $q, $exceptionHandler) { + this.$get = ['$rootScope', '$browser', '$q', '$$q', '$exceptionHandler', + function($rootScope, $browser, $q, $$q, $exceptionHandler) { var deferreds = {}; @@ -14201,9 +15088,9 @@ function $TimeoutProvider() { * */ function timeout(fn, delay, invokeApply) { - var deferred = $q.defer(), + var skipApply = (isDefined(invokeApply) && !invokeApply), + deferred = (skipApply ? $$q : $q).defer(), promise = deferred.promise, - skipApply = (isDefined(invokeApply) && !invokeApply), timeoutId; timeoutId = $browser.defer(function() { @@ -14400,6 +15287,17 @@ function $WindowProvider(){ this.$get = valueFn(window); } +/* global currencyFilter: true, + dateFilter: true, + filterFilter: true, + jsonFilter: true, + limitToFilter: true, + lowercaseFilter: true, + numberFilter: true, + orderByFilter: true, + uppercaseFilter: true, + */ + /** * @ngdoc provider * @name $filterProvider @@ -14449,16 +15347,6 @@ function $WindowProvider(){ * For more information about how angular filters work, and how to create your own filters, see * {@link guide/filter Filters} in the Angular Developer Guide. */ -/** - * @ngdoc method - * @name $filterProvider#register - * @description - * Register filter factory function. - * - * @param {String} name Name of the filter. - * @param {Function} fn The filter factory function which is injectable. - */ - /** * @ngdoc service @@ -14497,7 +15385,7 @@ function $FilterProvider($provide) { /** * @ngdoc method - * @name $controllerProvider#register + * @name $filterProvider#register * @param {string|Object} name Name of the filter function, or an object map of filters where * the keys are the filter names and the values are the filter factories. * @returns {Object} Registered filter instance, or if a map of filters was provided then a map @@ -14570,11 +15458,13 @@ function $FilterProvider($provide) { * which have property `name` containing "M" and property `phone` containing "1". A special * property name `$` can be used (as in `{$:"text"}`) to accept a match against any * property of the object. That's equivalent to the simple substring match with a `string` - * as described above. + * as described above. The predicate can be negated by prefixing the string with `!`. + * For Example `{name: "!M"}` predicate will return an array of items which have property `name` + * not containing "M". * - * - `function(value)`: A predicate function can be used to write arbitrary filters. The function is - * called for each element of `array`. The final result is an array of those elements that - * the predicate returned true for. + * - `function(value, index)`: A predicate function can be used to write arbitrary filters. The + * function is called for each element of `array`. The final result is an array of those + * elements that the predicate returned true for. * * @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 @@ -14667,9 +15557,9 @@ function filterFilter() { var comparatorType = typeof(comparator), predicates = []; - predicates.check = function(value) { + predicates.check = function(value, index) { for (var j = 0; j < predicates.length; j++) { - if(!predicates[j](value)) { + if(!predicates[j](value, index)) { return false; } } @@ -14758,7 +15648,7 @@ function filterFilter() { var filtered = []; for ( var j = 0; j < array.length; j++) { var value = array[j]; - if (predicates.check(value)) { + if (predicates.check(value, j)) { filtered.push(value); } } @@ -14819,8 +15709,12 @@ function currencyFilter($locale) { var formats = $locale.NUMBER_FORMATS; return function(amount, currencySymbol){ if (isUndefined(currencySymbol)) currencySymbol = formats.CURRENCY_SYM; - return formatNumber(amount, formats.PATTERNS[1], formats.GROUP_SEP, formats.DECIMAL_SEP, 2). - replace(/\u00A4/g, currencySymbol); + + // if null or undefined pass it through + return (amount == null) + ? amount + : formatNumber(amount, formats.PATTERNS[1], formats.GROUP_SEP, formats.DECIMAL_SEP, 2). + replace(/\u00A4/g, currencySymbol); }; } @@ -14879,14 +15773,18 @@ numberFilter.$inject = ['$locale']; function numberFilter($locale) { var formats = $locale.NUMBER_FORMATS; return function(number, fractionSize) { - return formatNumber(number, formats.PATTERNS[0], formats.GROUP_SEP, formats.DECIMAL_SEP, - fractionSize); + + // if null or undefined pass it through + return (number == null) + ? number + : formatNumber(number, formats.PATTERNS[0], formats.GROUP_SEP, formats.DECIMAL_SEP, + fractionSize); }; } var DECIMAL_SEP = '.'; function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) { - if (number == null || !isFinite(number) || isObject(number)) return ''; + if (!isFinite(number) || isObject(number)) return ''; var isNegative = number < 0; number = Math.abs(number); @@ -14919,6 +15817,10 @@ function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) { // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/round number = +(Math.round(+(number.toString() + 'e' + fractionSize)).toString() + 'e' + -fractionSize); + if (number === 0) { + isNegative = false; + } + var fraction = ('' + number).split(DECIMAL_SEP); var whole = fraction[0]; fraction = fraction[1] || ''; @@ -15007,6 +15909,32 @@ function timeZoneGetter(date) { return paddedZone; } +function getFirstThursdayOfYear(year) { + // 0 = index of January + var dayOfWeekOnFirst = (new Date(year, 0, 1)).getDay(); + // 4 = index of Thursday (+1 to account for 1st = 5) + // 11 = index of *next* Thursday (+1 account for 1st = 12) + return new Date(year, 0, ((dayOfWeekOnFirst <= 4) ? 5 : 12) - dayOfWeekOnFirst); +} + +function getThursdayThisWeek(datetime) { + return new Date(datetime.getFullYear(), datetime.getMonth(), + // 4 = index of Thursday + datetime.getDate() + (4 - datetime.getDay())); +} + +function weekGetter(size) { + return function(date) { + var firstThurs = getFirstThursdayOfYear(date.getFullYear()), + thisThurs = getThursdayThisWeek(date); + + var diff = +thisThurs - +firstThurs, + result = 1 + Math.round(diff / 6.048e8); // 6.048e8 ms per week + + return padNumber(result, size); + }; +} + function ampmGetter(date, formats) { return date.getHours() < 12 ? formats.AMPMS[0] : formats.AMPMS[1]; } @@ -15035,10 +15963,12 @@ var DATE_FORMATS = { EEEE: dateStrGetter('Day'), EEE: dateStrGetter('Day', true), a: ampmGetter, - Z: timeZoneGetter + Z: timeZoneGetter, + ww: weekGetter(2), + w: weekGetter(1) }; -var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z))(.*)/, +var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZEw']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z|w+))(.*)/, NUMBER_STRING = /^\-?\d+$/; /** @@ -15064,40 +15994,44 @@ var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+ * * `'EEE'`: Day in Week, (Sun-Sat) * * `'HH'`: Hour in day, padded (00-23) * * `'H'`: Hour in day (0-23) - * * `'hh'`: Hour in am/pm, padded (01-12) - * * `'h'`: Hour in am/pm, (1-12) + * * `'hh'`: Hour in AM/PM, padded (01-12) + * * `'h'`: Hour in AM/PM, (1-12) * * `'mm'`: Minute in hour, padded (00-59) * * `'m'`: Minute in hour (0-59) * * `'ss'`: Second in minute, padded (00-59) * * `'s'`: Second in minute (0-59) * * `'.sss' or ',sss'`: Millisecond in second, padded (000-999) - * * `'a'`: am/pm marker + * * `'a'`: AM/PM marker * * `'Z'`: 4 digit (+sign) representation of the timezone offset (-1200-+1200) + * * `'ww'`: ISO-8601 week of year (00-53) + * * `'w'`: ISO-8601 week of year (0-53) * * `format` string can also be one of the following predefined * {@link guide/i18n localizable formats}: * * * `'medium'`: equivalent to `'MMM d, y h:mm:ss a'` for en_US locale - * (e.g. Sep 3, 2010 12:05:08 pm) - * * `'short'`: equivalent to `'M/d/yy h:mm a'` for en_US locale (e.g. 9/3/10 12:05 pm) - * * `'fullDate'`: equivalent to `'EEEE, MMMM d,y'` for en_US locale + * (e.g. Sep 3, 2010 12:05:08 PM) + * * `'short'`: equivalent to `'M/d/yy h:mm a'` for en_US locale (e.g. 9/3/10 12:05 PM) + * * `'fullDate'`: equivalent to `'EEEE, MMMM d, y'` for en_US locale * (e.g. Friday, September 3, 2010) * * `'longDate'`: equivalent to `'MMMM d, y'` for en_US locale (e.g. September 3, 2010) * * `'mediumDate'`: equivalent to `'MMM d, y'` for en_US locale (e.g. Sep 3, 2010) * * `'shortDate'`: equivalent to `'M/d/yy'` for en_US locale (e.g. 9/3/10) - * * `'mediumTime'`: equivalent to `'h:mm:ss a'` for en_US locale (e.g. 12:05:08 pm) - * * `'shortTime'`: equivalent to `'h:mm a'` for en_US locale (e.g. 12:05 pm) + * * `'mediumTime'`: equivalent to `'h:mm:ss a'` for en_US locale (e.g. 12:05:08 PM) + * * `'shortTime'`: equivalent to `'h:mm a'` for en_US locale (e.g. 12:05 PM) * - * `format` string can contain literal values. These need to be quoted with single quotes (e.g. - * `"h 'in the morning'"`). In order to output single quote, use two single quotes in a sequence + * `format` string can contain literal values. These need to be escaped by surrounding with single quotes (e.g. + * `"h 'in the morning'"`). In order to output a single quote, escape it - i.e., two single quotes in a sequence * (e.g. `"h 'o''clock'"`). * * @param {(Date|number|string)} date Date to format either as Date object, milliseconds (string or - * number) or various ISO 8601 datetime string formats (e.g. yyyy-MM-ddTHH:mm:ss.SSSZ and its + * number) or various ISO 8601 datetime string formats (e.g. yyyy-MM-ddTHH:mm:ss.sssZ and its * shorter versions like yyyy-MM-ddTHH:mmZ, yyyy-MM-dd or yyyyMMddTHHmmssZ). If no timezone is * specified in the string input, the time is considered to be in the local timezone. * @param {string=} format Formatting rules (see Description). If not specified, * `mediumDate` is used. + * @param {string=} timezone Timezone to be used for formatting. Right now, only `'UTC'` is supported. + * If not specified, the timezone of the browser will be used. * @returns {string} Formatted string or the input if input is not recognized as date/millis. * * @example @@ -15109,6 +16043,8 @@ var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+ <span>{{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}</span><br> <span ng-non-bindable>{{1288323623006 | date:'MM/dd/yyyy @ h:mma'}}</span>: <span>{{'1288323623006' | date:'MM/dd/yyyy @ h:mma'}}</span><br> + <span ng-non-bindable>{{1288323623006 | date:"MM/dd/yyyy 'at' h:mma"}}</span>: + <span>{{'1288323623006' | date:"MM/dd/yyyy 'at' h:mma"}}</span><br> </file> <file name="protractor.js" type="protractor"> it('should format date', function() { @@ -15118,6 +16054,8 @@ var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+ toMatch(/2010\-10\-2\d \d{2}:\d{2}:\d{2} (\-|\+)?\d{4}/); expect(element(by.binding("'1288323623006' | date:'MM/dd/yyyy @ h:mma'")).getText()). toMatch(/10\/2\d\/2010 @ \d{1,2}:\d{2}(AM|PM)/); + expect(element(by.binding("'1288323623006' | date:\"MM/dd/yyyy 'at' h:mma\"")).getText()). + toMatch(/10\/2\d\/2010 at \d{1,2}:\d{2}(AM|PM)/); }); </file> </example> @@ -15153,7 +16091,7 @@ function dateFilter($locale) { } - return function(date, format) { + return function(date, format, timezone) { var text = '', parts = [], fn, match; @@ -15161,11 +16099,7 @@ function dateFilter($locale) { format = format || 'mediumDate'; format = $locale.DATETIME_FORMATS[format] || format; if (isString(date)) { - if (NUMBER_STRING.test(date)) { - date = int(date); - } else { - date = jsonStringToDate(date); - } + date = NUMBER_STRING.test(date) ? int(date) : jsonStringToDate(date); } if (isNumber(date)) { @@ -15187,6 +16121,10 @@ function dateFilter($locale) { } } + if (timezone && timezone === 'UTC') { + date = new Date(date.getTime()); + date.setMinutes(date.getMinutes() + date.getTimezoneOffset()); + } forEach(parts, function(value){ fn = DATE_FORMATS[value]; text += fn ? fn(date, $locale.DATETIME_FORMATS) @@ -15386,9 +16324,13 @@ function limitToFilter(){ * * - `function`: Getter function. The result of this function will be sorted using the * `<`, `=`, `>` operator. - * - `string`: An Angular expression which evaluates to an object to order by, such as 'name' - * to sort by a property called 'name'. Optionally prefixed with `+` or `-` to control - * ascending or descending sort order (for example, +name or -name). + * - `string`: An Angular expression. The result of this expression is used to compare elements + * (for example `name` to sort by a property called `name` or `name.substr(0, 3)` to sort by + * 3 first characters of a property called `name`). The result of a constant expression + * is interpreted as a property name to be used in comparisons (for example `"special name"` + * to sort object by the value of their `special name` property). An expression can be + * optionally prefixed with `+` or `-` to control ascending or descending sort order + * (for example, `+name` or `-name`). * - `Array`: An array of function or string predicates. The first predicate in the array * is used for sorting, but when two items are equivalent, the next predicate is used. * @@ -15440,7 +16382,7 @@ function limitToFilter(){ * @example <example module="orderByExample"> <file name="index.html"> - <div ng-controller="Ctrl"> + <div ng-controller="ExampleController"> <table class="friend"> <tr> <th><a href="" ng-click="reverse=false;order('name', false)">Name</a> @@ -15479,7 +16421,7 @@ function limitToFilter(){ orderByFilter.$inject = ['$parse']; function orderByFilter($parse){ return function(array, sortPredicate, reverseOrder) { - if (!isArray(array)) return array; + if (!(isArrayLike(array))) return array; if (!sortPredicate) return array; sortPredicate = isArray(sortPredicate) ? sortPredicate: [sortPredicate]; sortPredicate = map(sortPredicate, function(predicate){ @@ -15513,7 +16455,7 @@ function orderByFilter($parse){ return 0; } function reverseComparator(comp, descending) { - return toBoolean(descending) + return descending ? function(a,b){return comp(b,a);} : comp; } @@ -15521,6 +16463,10 @@ function orderByFilter($parse){ var t1 = typeof v1; var t2 = typeof v2; if (t1 == t2) { + if (isDate(v1) && isDate(v2)) { + v1 = v1.valueOf(); + v2 = v2.valueOf(); + } if (t1 == "string") { v1 = v1.toLowerCase(); v2 = v2.toLowerCase(); @@ -15609,12 +16555,12 @@ var htmlAnchorDirective = valueFn({ * * The wrong way to write it: * ```html - * <a href="http://www.gravatar.com/avatar/{{hash}}"/> + * <a href="http://www.gravatar.com/avatar/{{hash}}">link1</a> * ``` * * The correct way to write it: * ```html - * <a ng-href="http://www.gravatar.com/avatar/{{hash}}"/> + * <a ng-href="http://www.gravatar.com/avatar/{{hash}}">link1</a> * ``` * * @element A @@ -15752,7 +16698,7 @@ var htmlAnchorDirective = valueFn({ * * @description * - * The following markup will make the button enabled on Chrome/Firefox but not on IE8 and older IEs: + * We shouldn't do this, because it will make the button enabled on Chrome/Firefox but not on IE8 and older IEs: * ```html * <div ng-init="scope = { isDisabled: false }"> * <button disabled="{{scope.isDisabled}}">Disabled</button> @@ -15943,6 +16889,7 @@ forEach(BOOLEAN_ATTR, function(propName, attrName) { var normalized = directiveNormalize('ng-' + attrName); ngAttributeAliasDirectives[normalized] = function() { return { + restrict: 'A', priority: 100, link: function(scope, element, attr) { scope.$watch(attr[normalized], function ngBooleanAttrWatchAction(value) { @@ -15953,6 +16900,29 @@ forEach(BOOLEAN_ATTR, function(propName, attrName) { }; }); +// aliased input attrs are evaluated +forEach(ALIASED_ATTR, function(htmlAttr, ngAttr) { + ngAttributeAliasDirectives[ngAttr] = function() { + return { + priority: 100, + link: function(scope, element, attr) { + //special case ngPattern when a literal regular expression value + //is used as the expression (this way we don't have to watch anything). + if (ngAttr === "ngPattern" && attr.ngPattern.charAt(0) == "/") { + var match = attr.ngPattern.match(REGEX_STRING_REGEXP); + if (match) { + attr.$set("ngPattern", new RegExp(match[1], match[2])); + return; + } + } + + scope.$watch(attr[ngAttr], function ngAttrAliasWatchAction(value) { + attr.$set(ngAttr, value); + }); + } + }; + }; +}); // ng-src, ng-srcset, ng-href are interpolated forEach(['src', 'srcset', 'href'], function(attrName) { @@ -15972,8 +16942,12 @@ forEach(['src', 'srcset', 'href'], function(attrName) { } attr.$observe(normalized, function(value) { - if (!value) - return; + if (!value) { + if (attrName === 'href') { + attr.$set(name, null); + } + return; + } attr.$set(name, value); @@ -15988,14 +16962,19 @@ forEach(['src', 'srcset', 'href'], function(attrName) { }; }); -/* global -nullFormCtrl */ +/* global -nullFormCtrl, -SUBMITTED_CLASS, addSetValidityMethod: true + */ var nullFormCtrl = { $addControl: noop, $removeControl: noop, $setValidity: noop, + $$setPending: noop, $setDirty: noop, - $setPristine: noop -}; + $setPristine: noop, + $setSubmitted: noop, + $$clearControlValidity: noop +}, +SUBMITTED_CLASS = 'ng-submitted'; /** * @ngdoc type @@ -16005,13 +16984,13 @@ var nullFormCtrl = { * @property {boolean} $dirty True if user has already interacted with the form. * @property {boolean} $valid True if all of the containing forms and controls are valid. * @property {boolean} $invalid True if at least one containing control or form is invalid. + * @property {boolean} $submitted True if user has submitted the form even if its invalid. * - * @property {Object} $error Is an object hash, containing references to all invalid controls or - * forms, where: + * @property {Object} $error Is an object hash, containing references to controls or + * forms with failing validators, where: * * - keys are validation tokens (error names), - * - values are arrays of controls or forms that are invalid for given error name. - * + * - values are arrays of controls or forms that have a failing validator for given error name. * * Built-in validation tokens: * @@ -16038,29 +17017,57 @@ FormController.$inject = ['$element', '$attrs', '$scope', '$animate']; function FormController(element, attrs, $scope, $animate) { var form = this, parentForm = element.parent().controller('form') || nullFormCtrl, - invalidCount = 0, // used to easily determine if we are valid - errors = form.$error = {}, controls = []; // init state + form.$error = {}; + form.$$success = {}; + form.$pending = undefined; form.$name = attrs.name || attrs.ngForm; form.$dirty = false; form.$pristine = true; form.$valid = true; form.$invalid = false; + form.$submitted = false; parentForm.$addControl(form); // Setup initial state of the control element.addClass(PRISTINE_CLASS); - toggleValidCss(true); - // convenience method for easy toggling of classes - function toggleValidCss(isValid, validationErrorKey) { - validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : ''; - $animate.removeClass(element, (isValid ? INVALID_CLASS : VALID_CLASS) + validationErrorKey); - $animate.addClass(element, (isValid ? VALID_CLASS : INVALID_CLASS) + validationErrorKey); - } + /** + * @ngdoc method + * @name form.FormController#$rollbackViewValue + * + * @description + * Rollback all form controls pending updates to the `$modelValue`. + * + * Updates may be pending by a debounced event or because the input is waiting for a some future + * event defined in `ng-model-options`. This method is typically needed by the reset button of + * a form that uses `ng-model-options` to pend updates. + */ + form.$rollbackViewValue = function() { + forEach(controls, function(control) { + control.$rollbackViewValue(); + }); + }; + + /** + * @ngdoc method + * @name form.FormController#$commitViewValue + * + * @description + * Commit all form controls pending updates to the `$modelValue`. + * + * Updates may be pending by a debounced event or because the input is waiting for a some future + * event defined in `ng-model-options`. This method is rarely needed as `NgModelController` + * usually handles calling this in response to input events. + */ + form.$commitViewValue = function() { + forEach(controls, function(control) { + control.$commitViewValue(); + }); + }; /** * @ngdoc method @@ -16095,13 +17102,17 @@ function FormController(element, attrs, $scope, $animate) { if (control.$name && form[control.$name] === control) { delete form[control.$name]; } - forEach(errors, function(queue, validationToken) { - form.$setValidity(validationToken, true, control); + forEach(form.$pending, function(value, name) { + form.$setValidity(name, null, control); + }); + forEach(form.$error, function(value, name) { + form.$setValidity(name, null, control); }); arrayRemove(controls, control); }; + /** * @ngdoc method * @name form.FormController#$setValidity @@ -16111,43 +17122,33 @@ function FormController(element, attrs, $scope, $animate) { * * This method will also propagate to parent forms. */ - form.$setValidity = function(validationToken, isValid, control) { - var queue = errors[validationToken]; - - if (isValid) { - if (queue) { - arrayRemove(queue, control); - if (!queue.length) { - invalidCount--; - if (!invalidCount) { - toggleValidCss(isValid); - form.$valid = true; - form.$invalid = false; - } - errors[validationToken] = false; - toggleValidCss(true, validationToken); - parentForm.$setValidity(validationToken, true, form); + addSetValidityMethod({ + ctrl: this, + $element: element, + set: function(object, property, control) { + var list = object[property]; + if (!list) { + object[property] = [control]; + } else { + var index = list.indexOf(control); + if (index === -1) { + list.push(control); } } - - } else { - if (!invalidCount) { - toggleValidCss(isValid); + }, + unset: function(object, property, control) { + var list = object[property]; + if (!list) { + return; } - if (queue) { - if (includes(queue, control)) return; - } else { - errors[validationToken] = queue = []; - invalidCount++; - toggleValidCss(false, validationToken); - parentForm.$setValidity(validationToken, false, form); + arrayRemove(list, control); + if (list.length === 0) { + delete object[property]; } - queue.push(control); - - form.$valid = false; - form.$invalid = true; - } - }; + }, + parentForm: parentForm, + $animate: $animate + }); /** * @ngdoc method @@ -16182,16 +17183,28 @@ function FormController(element, attrs, $scope, $animate) { * saving or resetting it. */ form.$setPristine = function () { - $animate.removeClass(element, DIRTY_CLASS); - $animate.addClass(element, PRISTINE_CLASS); + $animate.setClass(element, PRISTINE_CLASS, DIRTY_CLASS + ' ' + SUBMITTED_CLASS); form.$dirty = false; form.$pristine = true; + form.$submitted = false; forEach(controls, function(control) { control.$setPristine(); }); }; -} + /** + * @ngdoc method + * @name form.FormController#$setSubmitted + * + * @description + * Sets the form to its submitted state. + */ + form.$setSubmitted = function () { + $animate.addClass(element, SUBMITTED_CLASS); + form.$submitted = true; + parentForm.$setSubmitted(); + }; +} /** * @ngdoc directive @@ -16241,6 +17254,7 @@ function FormController(element, attrs, $scope, $animate) { * - `ng-invalid` is set if the form is invalid. * - `ng-pristine` is set if the form is pristine. * - `ng-dirty` is set if the form is dirty. + * - `ng-submitted` is set if the form was submitted. * * Keep in mind that ngAnimate can detect each of these classes when added and removed. * @@ -16274,8 +17288,9 @@ function FormController(element, attrs, $scope, $animate) { * hitting enter in any of the input fields will trigger the click handler on the *first* button or * input[type=submit] (`ngClick`) *and* a submit handler on the enclosing form (`ngSubmit`) * - * @param {string=} name Name of the form. If specified, the form controller will be published into - * related scope, under this name. + * Any pending `ngModelOptions` changes will take place immediately when an enclosing form is + * submitted. Note that `ngClick` events will occur before the model is updated. Use `ngSubmit` + * to have access to the updated model. * * ## Animation Hooks * @@ -16353,6 +17368,8 @@ function FormController(element, attrs, $scope, $animate) { </file> </example> * + * @param {string=} name Name of the form. If specified, the form controller will be published into + * related scope, under this name. */ var formDirectiveFactory = function(isNgForm) { return ['$timeout', function($timeout) { @@ -16370,19 +17387,24 @@ var formDirectiveFactory = function(isNgForm) { // IE 9 is not affected because it doesn't fire a submit event and try to do a full // page reload if the form was destroyed by submission of the form via a click handler // on a button in the form. Looks like an IE9 specific bug. - var preventDefaultListener = function(event) { + var handleFormSubmission = function(event) { + scope.$apply(function() { + controller.$commitViewValue(); + controller.$setSubmitted(); + }); + event.preventDefault ? event.preventDefault() : event.returnValue = false; // IE }; - addEventListenerFn(formElement[0], 'submit', preventDefaultListener); + addEventListenerFn(formElement[0], 'submit', handleFormSubmission); // unregister the preventDefault listener so that we don't not leak memory but in a // way that will achieve the prevention of the default action. formElement.on('$destroy', function() { $timeout(function() { - removeEventListenerFn(formElement[0], 'submit', preventDefaultListener); + removeEventListenerFn(formElement[0], 'submit', handleFormSubmission); }, 0, false); }); } @@ -16414,17 +17436,27 @@ var formDirectiveFactory = function(isNgForm) { var formDirective = formDirectiveFactory(); var ngFormDirective = formDirectiveFactory(true); -/* global - - -VALID_CLASS, - -INVALID_CLASS, - -PRISTINE_CLASS, - -DIRTY_CLASS +/* global VALID_CLASS: true, + INVALID_CLASS: true, + PRISTINE_CLASS: true, + DIRTY_CLASS: true, + UNTOUCHED_CLASS: true, + TOUCHED_CLASS: true, */ +// Regex code is obtained from SO: https://stackoverflow.com/questions/3143070/javascript-regex-iso-datetime#answer-3143231 +var ISO_DATE_REGEXP = /\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z)/; var URL_REGEXP = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/; var EMAIL_REGEXP = /^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i; var NUMBER_REGEXP = /^\s*(\-|\+)?(\d+|(\d*(\.\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))?$/; +var WEEK_REGEXP = /^(\d{4})-W(\d\d)$/; +var MONTH_REGEXP = /^(\d{4})-(\d\d)$/; +var TIME_REGEXP = /^(\d\d):(\d\d)(?::(\d\d))?$/; +var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/; + +var $ngModelMinErr = new minErr('ngModel'); var inputType = { @@ -16433,7 +17465,9 @@ var inputType = { * @name input[text] * * @description - * Standard HTML text input with angular data binding. + * Standard HTML text input with angular data binding, inherited by most of the `input` elements. + * + * *NOTE* Not every feature offered is available for all input types. * * @param {string} ngModel Assignable angular expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. @@ -16451,6 +17485,8 @@ var inputType = { * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * @param {boolean=} [ngTrim=true] If set to false Angular will not automatically trim the input. + * This parameter is ignored for input[type=password] controls, which will never trim the + * input. * * @example <example name="text-input-directive" module="textInputExample"> @@ -16506,6 +17542,447 @@ var inputType = { */ 'text': textInputType, + /** + * @ngdoc input + * @name input[date] + * + * @description + * Input with date validation and transformation. In browsers that do not yet support + * the HTML5 date input, a text element will be used. In that case, text must be entered in a valid ISO-8601 + * date format (yyyy-MM-dd), for example: `2009-01-06`. Since many + * modern browsers do not yet support this input type, it is important to provide cues to users on the + * expected input format via a placeholder or label. The model must always be a Date object. + * + * The timezone to be used to read/write the `Date` instance in the model can be defined using + * {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser. + * + * @param {string} ngModel Assignable angular expression to data-bind to. + * @param {string=} name Property name of the form under which the control is published. + * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. This must be a + * valid ISO date string (yyyy-MM-dd). + * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. This must be + * a valid ISO date string (yyyy-MM-dd). + * @param {string=} required Sets `required` validation error key if the value is not entered. + * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to + * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of + * `required` when you want to data-bind to the `required` attribute. + * @param {string=} ngChange Angular expression to be executed when input changes due to user + * interaction with the input element. + * + * @example + <example name="date-input-directive" module="dateInputExample"> + <file name="index.html"> + <script> + angular.module('dateInputExample', []) + .controller('DateController', ['$scope', function($scope) { + $scope.value = new Date(2013, 9, 22); + }]); + </script> + <form name="myForm" ng-controller="DateController as dateCtrl"> + Pick a date in 2013: + <input type="date" id="exampleInput" name="input" ng-model="value" + placeholder="yyyy-MM-dd" min="2013-01-01" max="2013-12-31" required /> + <span class="error" ng-show="myForm.input.$error.required"> + Required!</span> + <span class="error" ng-show="myForm.input.$error.date"> + Not a valid date!</span> + <tt>value = {{value | date: "yyyy-MM-dd"}}</tt><br/> + <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/> + <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/> + <tt>myForm.$valid = {{myForm.$valid}}</tt><br/> + <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/> + </form> + </file> + <file name="protractor.js" type="protractor"> + var value = element(by.binding('value | date: "yyyy-MM-dd"')); + var valid = element(by.binding('myForm.input.$valid')); + var input = element(by.model('value')); + + // currently protractor/webdriver does not support + // sending keys to all known HTML5 input controls + // for various browsers (see https://github.com/angular/protractor/issues/562). + function setInput(val) { + // set the value of the element and force validation. + var scr = "var ipt = document.getElementById('exampleInput'); " + + "ipt.value = '" + val + "';" + + "angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });"; + browser.executeScript(scr); + } + + it('should initialize to model', function() { + expect(value.getText()).toContain('2013-10-22'); + expect(valid.getText()).toContain('myForm.input.$valid = true'); + }); + + it('should be invalid if empty', function() { + setInput(''); + expect(value.getText()).toEqual('value ='); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); + + it('should be invalid if over max', function() { + setInput('2015-01-01'); + expect(value.getText()).toContain(''); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); + </file> + </example> + */ + 'date': createDateInputType('date', DATE_REGEXP, + createDateParser(DATE_REGEXP, ['yyyy', 'MM', 'dd']), + 'yyyy-MM-dd'), + + /** + * @ngdoc input + * @name input[dateTimeLocal] + * + * @description + * Input with datetime validation and transformation. In browsers that do not yet support + * the HTML5 date input, a text element will be used. In that case, the text must be entered in a valid ISO-8601 + * local datetime format (yyyy-MM-ddTHH:mm:ss), for example: `2010-12-28T14:57:00`. The model must be a Date object. + * + * The timezone to be used to read/write the `Date` instance in the model can be defined using + * {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser. + * + * @param {string} ngModel Assignable angular expression to data-bind to. + * @param {string=} name Property name of the form under which the control is published. + * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. This must be a + * valid ISO datetime format (yyyy-MM-ddTHH:mm:ss). + * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. This must be + * a valid ISO datetime format (yyyy-MM-ddTHH:mm:ss). + * @param {string=} required Sets `required` validation error key if the value is not entered. + * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to + * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of + * `required` when you want to data-bind to the `required` attribute. + * @param {string=} ngChange Angular expression to be executed when input changes due to user + * interaction with the input element. + * + * @example + <example name="datetimelocal-input-directive" module="dateExample"> + <file name="index.html"> + <script> + angular.module('dateExample', []) + .controller('DateController', ['$scope', function($scope) { + $scope.value = new Date(2010, 11, 28, 14, 57); + }]); + </script> + <form name="myForm" ng-controller="DateController as dateCtrl"> + Pick a date between in 2013: + <input type="datetime-local" id="exampleInput" name="input" ng-model="value" + placeholder="yyyy-MM-ddTHH:mm:ss" min="2001-01-01T00:00:00" max="2013-12-31T00:00:00" required /> + <span class="error" ng-show="myForm.input.$error.required"> + Required!</span> + <span class="error" ng-show="myForm.input.$error.datetimelocal"> + Not a valid date!</span> + <tt>value = {{value | date: "yyyy-MM-ddTHH:mm:ss"}}</tt><br/> + <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/> + <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/> + <tt>myForm.$valid = {{myForm.$valid}}</tt><br/> + <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/> + </form> + </file> + <file name="protractor.js" type="protractor"> + var value = element(by.binding('value | date: "yyyy-MM-ddTHH:mm:ss"')); + var valid = element(by.binding('myForm.input.$valid')); + var input = element(by.model('value')); + + // currently protractor/webdriver does not support + // sending keys to all known HTML5 input controls + // for various browsers (https://github.com/angular/protractor/issues/562). + function setInput(val) { + // set the value of the element and force validation. + var scr = "var ipt = document.getElementById('exampleInput'); " + + "ipt.value = '" + val + "';" + + "angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });"; + browser.executeScript(scr); + } + + it('should initialize to model', function() { + expect(value.getText()).toContain('2010-12-28T14:57:00'); + expect(valid.getText()).toContain('myForm.input.$valid = true'); + }); + + it('should be invalid if empty', function() { + setInput(''); + expect(value.getText()).toEqual('value ='); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); + + it('should be invalid if over max', function() { + setInput('2015-01-01T23:59:00'); + expect(value.getText()).toContain(''); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); + </file> + </example> + */ + 'datetime-local': createDateInputType('datetimelocal', DATETIMELOCAL_REGEXP, + createDateParser(DATETIMELOCAL_REGEXP, ['yyyy', 'MM', 'dd', 'HH', 'mm', 'ss']), + 'yyyy-MM-ddTHH:mm:ss'), + + /** + * @ngdoc input + * @name input[time] + * + * @description + * Input with time validation and transformation. In browsers that do not yet support + * the HTML5 date input, a text element will be used. In that case, the text must be entered in a valid ISO-8601 + * local time format (HH:mm:ss), for example: `14:57:00`. Model must be a Date object. This binding will always output a + * Date object to the model of January 1, 1970, or local date `new Date(1970, 0, 1, HH, mm, ss)`. + * + * The timezone to be used to read/write the `Date` instance in the model can be defined using + * {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser. + * + * @param {string} ngModel Assignable angular expression to data-bind to. + * @param {string=} name Property name of the form under which the control is published. + * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. This must be a + * valid ISO time format (HH:mm:ss). + * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. This must be a + * valid ISO time format (HH:mm:ss). + * @param {string=} required Sets `required` validation error key if the value is not entered. + * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to + * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of + * `required` when you want to data-bind to the `required` attribute. + * @param {string=} ngChange Angular expression to be executed when input changes due to user + * interaction with the input element. + * + * @example + <example name="time-input-directive" module="timeExample"> + <file name="index.html"> + <script> + angular.module('timeExample', []) + .controller('DateController', ['$scope', function($scope) { + $scope.value = new Date(1970, 0, 1, 14, 57, 0); + }]); + </script> + <form name="myForm" ng-controller="DateController as dateCtrl"> + Pick a between 8am and 5pm: + <input type="time" id="exampleInput" name="input" ng-model="value" + placeholder="HH:mm:ss" min="08:00:00" max="17:00:00" required /> + <span class="error" ng-show="myForm.input.$error.required"> + Required!</span> + <span class="error" ng-show="myForm.input.$error.time"> + Not a valid date!</span> + <tt>value = {{value | date: "HH:mm:ss"}}</tt><br/> + <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/> + <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/> + <tt>myForm.$valid = {{myForm.$valid}}</tt><br/> + <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/> + </form> + </file> + <file name="protractor.js" type="protractor"> + var value = element(by.binding('value | date: "HH:mm:ss"')); + var valid = element(by.binding('myForm.input.$valid')); + var input = element(by.model('value')); + + // currently protractor/webdriver does not support + // sending keys to all known HTML5 input controls + // for various browsers (https://github.com/angular/protractor/issues/562). + function setInput(val) { + // set the value of the element and force validation. + var scr = "var ipt = document.getElementById('exampleInput'); " + + "ipt.value = '" + val + "';" + + "angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });"; + browser.executeScript(scr); + } + + it('should initialize to model', function() { + expect(value.getText()).toContain('14:57:00'); + expect(valid.getText()).toContain('myForm.input.$valid = true'); + }); + + it('should be invalid if empty', function() { + setInput(''); + expect(value.getText()).toEqual('value ='); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); + + it('should be invalid if over max', function() { + setInput('23:59:00'); + expect(value.getText()).toContain(''); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); + </file> + </example> + */ + 'time': createDateInputType('time', TIME_REGEXP, + createDateParser(TIME_REGEXP, ['HH', 'mm', 'ss']), + 'HH:mm:ss'), + + /** + * @ngdoc input + * @name input[week] + * + * @description + * Input with week-of-the-year validation and transformation to Date. In browsers that do not yet support + * the HTML5 week input, a text element will be used. In that case, the text must be entered in a valid ISO-8601 + * week format (yyyy-W##), for example: `2013-W02`. The model must always be a Date object. + * + * The timezone to be used to read/write the `Date` instance in the model can be defined using + * {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser. + * + * @param {string} ngModel Assignable angular expression to data-bind to. + * @param {string=} name Property name of the form under which the control is published. + * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. This must be a + * valid ISO week format (yyyy-W##). + * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. This must be + * a valid ISO week format (yyyy-W##). + * @param {string=} required Sets `required` validation error key if the value is not entered. + * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to + * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of + * `required` when you want to data-bind to the `required` attribute. + * @param {string=} ngChange Angular expression to be executed when input changes due to user + * interaction with the input element. + * + * @example + <example name="week-input-directive" module="weekExample"> + <file name="index.html"> + <script> + angular.module('weekExample', []) + .controller('DateController', ['$scope', function($scope) { + $scope.value = new Date(2013, 0, 3); + }]); + </script> + <form name="myForm" ng-controller="DateController as dateCtrl"> + Pick a date between in 2013: + <input id="exampleInput" type="week" name="input" ng-model="value" + placeholder="YYYY-W##" min="2012-W32" max="2013-W52" required /> + <span class="error" ng-show="myForm.input.$error.required"> + Required!</span> + <span class="error" ng-show="myForm.input.$error.week"> + Not a valid date!</span> + <tt>value = {{value | date: "yyyy-Www"}}</tt><br/> + <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/> + <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/> + <tt>myForm.$valid = {{myForm.$valid}}</tt><br/> + <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/> + </form> + </file> + <file name="protractor.js" type="protractor"> + var value = element(by.binding('value | date: "yyyy-Www"')); + var valid = element(by.binding('myForm.input.$valid')); + var input = element(by.model('value')); + + // currently protractor/webdriver does not support + // sending keys to all known HTML5 input controls + // for various browsers (https://github.com/angular/protractor/issues/562). + function setInput(val) { + // set the value of the element and force validation. + var scr = "var ipt = document.getElementById('exampleInput'); " + + "ipt.value = '" + val + "';" + + "angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });"; + browser.executeScript(scr); + } + + it('should initialize to model', function() { + expect(value.getText()).toContain('2013-W01'); + expect(valid.getText()).toContain('myForm.input.$valid = true'); + }); + + it('should be invalid if empty', function() { + setInput(''); + expect(value.getText()).toEqual('value ='); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); + + it('should be invalid if over max', function() { + setInput('2015-W01'); + expect(value.getText()).toContain(''); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); + </file> + </example> + */ + 'week': createDateInputType('week', WEEK_REGEXP, weekParser, 'yyyy-Www'), + + /** + * @ngdoc input + * @name input[month] + * + * @description + * Input with month validation and transformation. In browsers that do not yet support + * the HTML5 month input, a text element will be used. In that case, the text must be entered in a valid ISO-8601 + * month format (yyyy-MM), for example: `2009-01`. The model must always be a Date object. In the event the model is + * not set to the first of the month, the first of that model's month is assumed. + * + * The timezone to be used to read/write the `Date` instance in the model can be defined using + * {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser. + * + * @param {string} ngModel Assignable angular expression to data-bind to. + * @param {string=} name Property name of the form under which the control is published. + * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. This must be + * a valid ISO month format (yyyy-MM). + * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. This must + * be a valid ISO month format (yyyy-MM). + * @param {string=} required Sets `required` validation error key if the value is not entered. + * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to + * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of + * `required` when you want to data-bind to the `required` attribute. + * @param {string=} ngChange Angular expression to be executed when input changes due to user + * interaction with the input element. + * + * @example + <example name="month-input-directive" module="monthExample"> + <file name="index.html"> + <script> + angular.module('monthExample', []) + .controller('DateController', ['$scope', function($scope) { + $scope.value = new Date(2013, 9, 1); + }]); + </script> + <form name="myForm" ng-controller="DateController as dateCtrl"> + Pick a month int 2013: + <input id="exampleInput" type="month" name="input" ng-model="value" + placeholder="yyyy-MM" min="2013-01" max="2013-12" required /> + <span class="error" ng-show="myForm.input.$error.required"> + Required!</span> + <span class="error" ng-show="myForm.input.$error.month"> + Not a valid month!</span> + <tt>value = {{value | date: "yyyy-MM"}}</tt><br/> + <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/> + <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/> + <tt>myForm.$valid = {{myForm.$valid}}</tt><br/> + <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/> + </form> + </file> + <file name="protractor.js" type="protractor"> + var value = element(by.binding('value | date: "yyyy-MM"')); + var valid = element(by.binding('myForm.input.$valid')); + var input = element(by.model('value')); + + // currently protractor/webdriver does not support + // sending keys to all known HTML5 input controls + // for various browsers (https://github.com/angular/protractor/issues/562). + function setInput(val) { + // set the value of the element and force validation. + var scr = "var ipt = document.getElementById('exampleInput'); " + + "ipt.value = '" + val + "';" + + "angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });"; + browser.executeScript(scr); + } + + it('should initialize to model', function() { + expect(value.getText()).toContain('2013-10'); + expect(valid.getText()).toContain('myForm.input.$valid = true'); + }); + + it('should be invalid if empty', function() { + setInput(''); + expect(value.getText()).toEqual('value ='); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); + + it('should be invalid if over max', function() { + setInput('2015-01'); + expect(value.getText()).toContain(''); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); + </file> + </example> + */ + 'month': createDateInputType('month', MONTH_REGEXP, + createDateParser(MONTH_REGEXP, ['yyyy', 'MM']), + 'yyyy-MM'), /** * @ngdoc input @@ -16799,8 +18276,8 @@ 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=} ngTrueValue The value to which the expression should be set when selected. - * @param {string=} ngFalseValue The value to which the expression should be set when not selected. + * @param {expression=} ngTrueValue The value to which the expression should be set when selected. + * @param {expression=} ngFalseValue The value to which the expression should be set when not selected. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * @@ -16817,7 +18294,7 @@ var inputType = { <form name="myForm" ng-controller="ExampleController"> Value1: <input type="checkbox" ng-model="value1"> <br/> Value2: <input type="checkbox" ng-model="value2" - ng-true-value="YES" ng-false-value="NO"> <br/> + ng-true-value="'YES'" ng-false-value="'NO'"> <br/> <tt>value1 = {{value1}}</tt><br/> <tt>value2 = {{value2}}</tt><br/> </form> @@ -16848,13 +18325,6 @@ var inputType = { 'file': noop }; -// A helper function to call $setValidity and return the value / undefined, -// a pattern that is repeated a lot in the input validation logic. -function validate(ctrl, validatorName, validity, value){ - ctrl.$setValidity(validatorName, validity); - return validity ? value : undefined; -} - function testFlags(validity, flags) { var i, flag; if (flags) { @@ -16868,29 +18338,21 @@ function testFlags(validity, flags) { return false; } -// Pass validity so that behaviour can be mocked easier. -function addNativeHtml5Validators(ctrl, validatorName, badFlags, ignoreFlags, validity) { - if (isObject(validity)) { - ctrl.$$hasNativeValidators = true; - var validator = function(value) { - // Don't overwrite previous validation, don't consider valueMissing to apply (ng-required can - // perform the required validation) - if (!ctrl.$error[validatorName] && - !testFlags(validity, ignoreFlags) && - testFlags(validity, badFlags)) { - ctrl.$setValidity(validatorName, false); - return; - } - return value; - }; - ctrl.$parsers.push(validator); - } +function stringBasedInputType(ctrl) { + ctrl.$formatters.push(function(value) { + return ctrl.$isEmpty(value) ? value : value.toString(); + }); } function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { + baseInputType(scope, element, attr, ctrl, $sniffer, $browser); + stringBasedInputType(ctrl); +} + +function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) { var validity = element.prop(VALIDITY_STATE_PROPERTY); var placeholder = element[0].placeholder, noevent = {}; - ctrl.$$validityState = validity; + var type = lowercase(element[0].type); // In composition mode, users are still inputing intermediate text buffer, // hold the listener until composition is done. @@ -16910,7 +18372,8 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { var listener = function(ev) { if (composing) return; - var value = element.val(); + var value = element.val(), + event = ev && ev.type; // IE (11 and under) seem to emit an 'input' event if the placeholder value changes. // We don't want to dirty the value when this happens, so we abort here. Unfortunately, @@ -16923,23 +18386,16 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { // By default we will trim the value // If the attribute ng-trim exists we will avoid trimming - // e.g. <input ng-model="foo" ng-trim="false"> - if (toBoolean(attr.ngTrim || 'T')) { + // If input type is 'password', the value is never trimmed + if (type !== 'password' && (!attr.ngTrim || attr.ngTrim !== 'false')) { value = trim(value); } - // If a control is suffering from bad input, browsers discard its value, so it may be - // necessary to revalidate even if the control's value is the same empty value twice in - // a row. - var revalidate = validity && ctrl.$$hasNativeValidators; - if (ctrl.$viewValue !== value || (value === '' && revalidate)) { - if (scope.$$phase) { - ctrl.$setViewValue(value); - } else { - scope.$apply(function() { - ctrl.$setViewValue(value); - }); - } + // If a control is suffering from bad input (due to native validators), browsers discard its + // value, so it may be necessary to revalidate (by calling $setViewValue again) even if the + // control's value is the same empty value twice in a row. + if (ctrl.$viewValue !== value || (value === '' && ctrl.$$hasNativeValidators)) { + ctrl.$setViewValue(value, event); } }; @@ -16950,10 +18406,10 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { } else { var timeout; - var deferListener = function() { + var deferListener = function(ev) { if (!timeout) { timeout = $browser.defer(function() { - listener(); + listener(ev); timeout = null; }); } @@ -16966,7 +18422,7 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { // command modifiers arrows if (key === 91 || (15 < key && key < 19) || (37 <= key && key <= 40)) return; - deferListener(); + deferListener(event); }); // if user modifies input value using context menu in IE, we need "paste" and "cut" events to catch it @@ -16982,129 +18438,213 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { ctrl.$render = function() { element.val(ctrl.$isEmpty(ctrl.$viewValue) ? '' : ctrl.$viewValue); }; +} + +function weekParser(isoWeek) { + if (isDate(isoWeek)) { + return isoWeek; + } - // pattern validator - var pattern = attr.ngPattern, - patternValidator, - match; + if (isString(isoWeek)) { + WEEK_REGEXP.lastIndex = 0; + var parts = WEEK_REGEXP.exec(isoWeek); + if (parts) { + var year = +parts[1], + week = +parts[2], + firstThurs = getFirstThursdayOfYear(year), + addDays = (week - 1) * 7; + return new Date(year, 0, firstThurs.getDate() + addDays); + } + } - if (pattern) { - var validateRegex = function(regexp, value) { - return validate(ctrl, 'pattern', ctrl.$isEmpty(value) || regexp.test(value), value); - }; - match = pattern.match(/^\/(.*)\/([gim]*)$/); - if (match) { - pattern = new RegExp(match[1], match[2]); - patternValidator = function(value) { - return validateRegex(pattern, value); - }; - } else { - patternValidator = function(value) { - var patternObj = scope.$eval(pattern); + return NaN; +} - if (!patternObj || !patternObj.test) { - throw minErr('ngPattern')('noregexp', - 'Expected {0} to be a RegExp but was {1}. Element: {2}', pattern, - patternObj, startingTag(element)); - } - return validateRegex(patternObj, value); - }; +function createDateParser(regexp, mapping) { + return function(iso) { + var parts, map; + + if (isDate(iso)) { + return iso; } - ctrl.$formatters.push(patternValidator); - ctrl.$parsers.push(patternValidator); - } + if (isString(iso)) { + // When a date is JSON'ified to wraps itself inside of an extra + // set of double quotes. This makes the date parsing code unable + // to match the date string and parse it as a date. + if (iso.charAt(0) == '"' && iso.charAt(iso.length-1) == '"') { + iso = iso.substring(1, iso.length-1); + } + if (ISO_DATE_REGEXP.test(iso)) { + return new Date(iso); + } + regexp.lastIndex = 0; + parts = regexp.exec(iso); - // min length validator - if (attr.ngMinlength) { - var minlength = int(attr.ngMinlength); - var minLengthValidator = function(value) { - return validate(ctrl, 'minlength', ctrl.$isEmpty(value) || value.length >= minlength, value); - }; + if (parts) { + parts.shift(); + map = { yyyy: 1970, MM: 1, dd: 1, HH: 0, mm: 0, ss: 0 }; - ctrl.$parsers.push(minLengthValidator); - ctrl.$formatters.push(minLengthValidator); - } + forEach(parts, function(part, index) { + if (index < mapping.length) { + map[mapping[index]] = +part; + } + }); + return new Date(map.yyyy, map.MM - 1, map.dd, map.HH, map.mm, map.ss || 0); + } + } - // max length validator - if (attr.ngMaxlength) { - var maxlength = int(attr.ngMaxlength); - var maxLengthValidator = function(value) { - return validate(ctrl, 'maxlength', ctrl.$isEmpty(value) || value.length <= maxlength, value); - }; + return NaN; + }; +} - ctrl.$parsers.push(maxLengthValidator); - ctrl.$formatters.push(maxLengthValidator); - } +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; + + ctrl.$$parserName = type; + ctrl.$parsers.push(function(value) { + if (ctrl.$isEmpty(value)) return null; + if (regexp.test(value)) { + var parsedDate = parseDate(value); + if (timezone === 'UTC') { + parsedDate.setMinutes(parsedDate.getMinutes() - parsedDate.getTimezoneOffset()); + } + return parsedDate; + } + return undefined; + }); + + ctrl.$formatters.push(function(value) { + if (isDate(value)) { + return $filter('date')(value, format, timezone); + } + return ''; + }); + + if (isDefined(attr.min) || attr.ngMin) { + var minVal; + ctrl.$validators.min = function(value) { + return ctrl.$isEmpty(value) || isUndefined(minVal) || parseDate(value) >= minVal; + }; + attr.$observe('min', function(val) { + minVal = parseObservedDateValue(val); + ctrl.$validate(); + }); + } + + if (isDefined(attr.max) || attr.ngMax) { + var maxVal; + ctrl.$validators.max = function(value) { + return ctrl.$isEmpty(value) || isUndefined(maxVal) || parseDate(value) <= maxVal; + }; + attr.$observe('max', function(val) { + maxVal = parseObservedDateValue(val); + ctrl.$validate(); + }); + } + + function parseObservedDateValue(val) { + return isDefined(val) ? (isDate(val) ? val : parseDate(val)) : undefined; + } + }; } -var numberBadFlags = ['badInput']; +function badInputChecker(scope, element, attr, ctrl) { + var node = element[0]; + var nativeValidation = ctrl.$$hasNativeValidators = isObject(node.validity); + if (nativeValidation) { + ctrl.$parsers.push(function(value) { + var validity = element.prop(VALIDITY_STATE_PROPERTY) || {}; + // Detect bug in FF35 for input[email] (https://bugzilla.mozilla.org/show_bug.cgi?id=1064430): + // - also sets validity.badInput (should only be validity.typeMismatch). + // - see http://www.whatwg.org/specs/web-apps/current-work/multipage/forms.html#e-mail-state-(type=email) + // - can ignore this case as we can still read out the erroneous email... + return validity.badInput && !validity.typeMismatch ? undefined : value; + }); + } +} function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { - textInputType(scope, element, attr, ctrl, $sniffer, $browser); + badInputChecker(scope, element, attr, ctrl); + baseInputType(scope, element, attr, ctrl, $sniffer, $browser); + ctrl.$$parserName = 'number'; ctrl.$parsers.push(function(value) { - var empty = ctrl.$isEmpty(value); - if (empty || NUMBER_REGEXP.test(value)) { - ctrl.$setValidity('number', true); - return value === '' ? null : (empty ? value : parseFloat(value)); - } else { - ctrl.$setValidity('number', false); - return undefined; - } + if (ctrl.$isEmpty(value)) return null; + if (NUMBER_REGEXP.test(value)) return parseFloat(value); + return undefined; }); - addNativeHtml5Validators(ctrl, 'number', numberBadFlags, null, ctrl.$$validityState); - ctrl.$formatters.push(function(value) { - return ctrl.$isEmpty(value) ? '' : '' + value; + if (!ctrl.$isEmpty(value)) { + if (!isNumber(value)) { + throw $ngModelMinErr('numfmt', 'Expected `{0}` to be a number', value); + } + value = value.toString(); + } + return value; }); - if (attr.min) { - var minValidator = function(value) { - var min = parseFloat(attr.min); - return validate(ctrl, 'min', ctrl.$isEmpty(value) || value >= min, value); + if (attr.min || attr.ngMin) { + var minVal; + ctrl.$validators.min = function(value) { + return ctrl.$isEmpty(value) || isUndefined(minVal) || value >= minVal; }; - ctrl.$parsers.push(minValidator); - ctrl.$formatters.push(minValidator); + attr.$observe('min', function(val) { + if (isDefined(val) && !isNumber(val)) { + val = parseFloat(val, 10); + } + minVal = isNumber(val) && !isNaN(val) ? val : undefined; + // TODO(matsko): implement validateLater to reduce number of validations + ctrl.$validate(); + }); } - if (attr.max) { - var maxValidator = function(value) { - var max = parseFloat(attr.max); - return validate(ctrl, 'max', ctrl.$isEmpty(value) || value <= max, value); + if (attr.max || attr.ngMax) { + var maxVal; + ctrl.$validators.max = function(value) { + return ctrl.$isEmpty(value) || isUndefined(maxVal) || value <= maxVal; }; - ctrl.$parsers.push(maxValidator); - ctrl.$formatters.push(maxValidator); + attr.$observe('max', function(val) { + if (isDefined(val) && !isNumber(val)) { + val = parseFloat(val, 10); + } + maxVal = isNumber(val) && !isNaN(val) ? val : undefined; + // TODO(matsko): implement validateLater to reduce number of validations + ctrl.$validate(); + }); } - - ctrl.$formatters.push(function(value) { - return validate(ctrl, 'number', ctrl.$isEmpty(value) || isNumber(value), value); - }); } function urlInputType(scope, element, attr, ctrl, $sniffer, $browser) { - textInputType(scope, element, attr, ctrl, $sniffer, $browser); - - var urlValidator = function(value) { - return validate(ctrl, 'url', ctrl.$isEmpty(value) || URL_REGEXP.test(value), value); + // Note: no badInputChecker here by purpose as `url` is only a validation + // in browsers, i.e. we can always read out input.value even if it is not valid! + baseInputType(scope, element, attr, ctrl, $sniffer, $browser); + stringBasedInputType(ctrl); + + ctrl.$$parserName = 'url'; + ctrl.$validators.url = function(modelValue, viewValue) { + var value = modelValue || viewValue; + return ctrl.$isEmpty(value) || URL_REGEXP.test(value); }; - - ctrl.$formatters.push(urlValidator); - ctrl.$parsers.push(urlValidator); } function emailInputType(scope, element, attr, ctrl, $sniffer, $browser) { - textInputType(scope, element, attr, ctrl, $sniffer, $browser); - - var emailValidator = function(value) { - return validate(ctrl, 'email', ctrl.$isEmpty(value) || EMAIL_REGEXP.test(value), value); + // Note: no badInputChecker here by purpose as `url` is only a validation + // in browsers, i.e. we can always read out input.value even if it is not valid! + baseInputType(scope, element, attr, ctrl, $sniffer, $browser); + stringBasedInputType(ctrl); + + ctrl.$$parserName = 'email'; + ctrl.$validators.email = function(modelValue, viewValue) { + var value = modelValue || viewValue; + return ctrl.$isEmpty(value) || EMAIL_REGEXP.test(value); }; - - ctrl.$formatters.push(emailValidator); - ctrl.$parsers.push(emailValidator); } function radioInputType(scope, element, attr, ctrl) { @@ -17113,13 +18653,13 @@ function radioInputType(scope, element, attr, ctrl) { element.attr('name', nextUid()); } - element.on('click', function() { + var listener = function(ev) { if (element[0].checked) { - scope.$apply(function() { - ctrl.$setViewValue(attr.value); - }); + ctrl.$setViewValue(attr.value, ev && ev.type); } - }); + }; + + element.on('click', listener); ctrl.$render = function() { var value = attr.value; @@ -17129,18 +18669,28 @@ function radioInputType(scope, element, attr, ctrl) { attr.$observe('value', ctrl.$render); } -function checkboxInputType(scope, element, attr, ctrl) { - var trueValue = attr.ngTrueValue, - falseValue = attr.ngFalseValue; +function parseConstantExpr($parse, context, name, expression, fallback) { + var parseFn; + if (isDefined(expression)) { + parseFn = $parse(expression); + if (!parseFn.constant) { + throw minErr('ngModel')('constexpr', 'Expected constant expression for `{0}`, but saw ' + + '`{1}`.', name, expression); + } + return parseFn(context); + } + return fallback; +} - if (!isString(trueValue)) trueValue = true; - if (!isString(falseValue)) falseValue = false; +function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter, $parse) { + var trueValue = parseConstantExpr($parse, scope, 'ngTrueValue', attr.ngTrueValue, true); + var falseValue = parseConstantExpr($parse, scope, 'ngFalseValue', attr.ngFalseValue, false); - element.on('click', function() { - scope.$apply(function() { - ctrl.$setViewValue(element[0].checked); - }); - }); + var listener = function(ev) { + ctrl.$setViewValue(element[0].checked, ev && ev.type); + }; + + element.on('click', listener); ctrl.$render = function() { element[0].checked = ctrl.$viewValue; @@ -17152,7 +18702,7 @@ function checkboxInputType(scope, element, attr, ctrl) { }; ctrl.$formatters.push(function(value) { - return value === trueValue; + return equals(value, trueValue); }); ctrl.$parsers.push(function(value) { @@ -17199,6 +18749,8 @@ function checkboxInputType(scope, element, attr, ctrl) { * HTML input element control with angular data-binding. Input control follows HTML5 input types * and polyfills the HTML5 validation behavior for older browsers. * + * *NOTE* Not every feature offered is available for all input types. + * * @param {string} ngModel Assignable angular expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. * @param {string=} required Sets `required` validation error key if the value is not entered. @@ -17212,6 +18764,9 @@ function checkboxInputType(scope, element, attr, ctrl) { * patterns defined as scope expressions. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. + * @param {boolean=} [ngTrim=true] If set to false Angular will not automatically trim the input. + * This parameter is ignored for input[type=password] controls, which will never trim the + * input. * * @example <example name="input-directive" module="inputExample"> @@ -17247,7 +18802,7 @@ function checkboxInputType(scope, element, attr, ctrl) { </div> </file> <file name="protractor.js" type="protractor"> - var user = element(by.binding('{{user}}')); + var user = element(by.exactBinding('user')); var userNameValid = element(by.binding('myForm.userName.$valid')); var lastNameValid = element(by.binding('myForm.lastName.$valid')); var lastNameError = element(by.binding('myForm.lastName.$error')); @@ -17301,14 +18856,15 @@ function checkboxInputType(scope, element, attr, ctrl) { </file> </example> */ -var inputDirective = ['$browser', '$sniffer', function($browser, $sniffer) { +var inputDirective = ['$browser', '$sniffer', '$filter', '$parse', + function($browser, $sniffer, $filter, $parse) { return { restrict: 'E', - require: '?ngModel', - link: function(scope, element, attr, ctrl) { - if (ctrl) { - (inputType[lowercase(attr.type)] || inputType.text)(scope, element, attr, ctrl, $sniffer, - $browser); + require: ['?ngModel'], + link: function(scope, element, attr, ctrls) { + if (ctrls[0]) { + (inputType[lowercase(attr.type)] || inputType.text)(scope, element, attr, ctrls[0], $sniffer, + $browser, $filter, $parse); } } }; @@ -17317,7 +18873,10 @@ var inputDirective = ['$browser', '$sniffer', function($browser, $sniffer) { var VALID_CLASS = 'ng-valid', INVALID_CLASS = 'ng-invalid', PRISTINE_CLASS = 'ng-pristine', - DIRTY_CLASS = 'ng-dirty'; + DIRTY_CLASS = 'ng-dirty', + UNTOUCHED_CLASS = 'ng-untouched', + TOUCHED_CLASS = 'ng-touched', + PENDING_CLASS = 'ng-pending'; /** * @ngdoc type @@ -17346,12 +18905,61 @@ var VALID_CLASS = 'ng-valid', * ngModel.$formatters.push(formatter); * ``` * + * @property {Object.<string, function>} $validators A collection of validators that are applied + * whenever the model value changes. The key value within the object refers to the name of the + * validator while the function refers to the validation operation. The validation operation is + * provided with the model value as an argument and must return a true or false value depending + * on the response of that validation. + * + * ```js + * ngModel.$validators.validCharacters = function(modelValue, viewValue) { + * var value = modelValue || viewValue; + * return /[0-9]+/.test(value) && + * /[a-z]+/.test(value) && + * /[A-Z]+/.test(value) && + * /\W+/.test(value); + * }; + * ``` + * + * @property {Object.<string, function>} $asyncValidators A collection of validations that are expected to + * perform an asynchronous validation (e.g. a HTTP request). The validation function that is provided + * is expected to return a promise when it is run during the model validation process. Once the promise + * is delivered then the validation status will be set to true when fulfilled and false when rejected. + * When the asynchronous validators are triggered, each of the validators will run in parallel and the model + * value will only be updated once all validators have been fulfilled. Also, keep in mind that all + * asynchronous validators will only run once all synchronous validators have passed. + * + * Please note that if $http is used then it is important that the server returns a success HTTP response code + * in order to fulfill the validation and a status level of `4xx` in order to reject the validation. + * + * ```js + * ngModel.$asyncValidators.uniqueUsername = function(modelValue, viewValue) { + * var value = modelValue || viewValue; + * + * // Lookup user by username + * return $http.get('/api/users/' + value). + * then(function resolved() { + * //username exists, this means validation fails + * return $q.reject('exists'); + * }, function rejected() { + * //username does not exist, therefore this validation passes + * return true; + * }); + * }; + * ``` + * + * @param {string} name The name of the validator. + * @param {Function} validationFn The validation function that will be run. + * * @property {Array.<Function>} $viewChangeListeners Array of functions to execute whenever the * view value has changed. It is called with no arguments, and its return value is ignored. * This can be used in place of additional $watches against the model value. * - * @property {Object} $error An object hash with all errors as keys. + * @property {Object} $error An object hash with all failing validator ids as keys. + * @property {Object} $pending An object hash with all pending validator ids as keys. * + * @property {boolean} $untouched True if control has not lost focus yet. + * @property {boolean} $touched True if control has lost focus. * @property {boolean} $pristine True if user has not interacted with the control yet. * @property {boolean} $dirty True if user has already interacted with the control. * @property {boolean} $valid True if there is no error. @@ -17398,7 +19006,7 @@ var VALID_CLASS = 'ng-valid', restrict: 'A', // only activate on element attribute require: '?ngModel', // get a hold of NgModelController link: function(scope, element, attrs, ngModel) { - if(!ngModel) return; // do nothing if no ng-model + if (!ngModel) return; // do nothing if no ng-model // Specify how UI should be updated ngModel.$render = function() { @@ -17416,7 +19024,7 @@ var VALID_CLASS = 'ng-valid', 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); @@ -17458,26 +19066,58 @@ var VALID_CLASS = 'ng-valid', * * */ -var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', '$animate', - function($scope, $exceptionHandler, $attr, $element, $parse, $animate) { +var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', '$animate', '$timeout', '$rootScope', '$q', + function($scope, $exceptionHandler, $attr, $element, $parse, $animate, $timeout, $rootScope, $q) { this.$viewValue = Number.NaN; this.$modelValue = Number.NaN; + this.$validators = {}; + this.$asyncValidators = {}; this.$parsers = []; this.$formatters = []; this.$viewChangeListeners = []; + this.$untouched = true; + this.$touched = false; this.$pristine = true; this.$dirty = false; this.$valid = true; this.$invalid = false; + this.$error = {}; // keep invalid keys here + this.$$success = {}; // keep valid keys here + this.$pending = undefined; // keep pending keys here this.$name = $attr.name; - var ngModelGet = $parse($attr.ngModel), - ngModelSet = ngModelGet.assign; - if (!ngModelSet) { - throw minErr('ngModel')('nonassign', "Expression '{0}' is non-assignable. Element: {1}", - $attr.ngModel, startingTag($element)); - } + var parsedNgModel = $parse($attr.ngModel), + pendingDebounce = null, + ctrl = this; + + var ngModelGet = function ngModelGet() { + var modelValue = parsedNgModel($scope); + if (ctrl.$options && ctrl.$options.getterSetter && isFunction(modelValue)) { + modelValue = modelValue(); + } + return modelValue; + }; + + var ngModelSet = function ngModelSet(newValue) { + var getterSetter; + if (ctrl.$options && ctrl.$options.getterSetter && + isFunction(getterSetter = parsedNgModel($scope))) { + + getterSetter(ctrl.$modelValue); + } else { + parsedNgModel.assign($scope, ctrl.$modelValue); + } + }; + + this.$$setOptions = function(options) { + ctrl.$options = options; + + if (!parsedNgModel.assign && (!options || !options.getterSetter)) { + throw $ngModelMinErr('nonassign', "Expression '{0}' is non-assignable. Element: {1}", + $attr.ngModel, startingTag($element)); + } + }; /** * @ngdoc method @@ -17486,6 +19126,18 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * @description * Called when the view needs to be updated. It is expected that the user of the ng-model * directive will implement this method. + * + * The `$render()` method is invoked in the following situations: + * + * * `$rollbackViewValue()` is called. If we are rolling back the view value to the last + * committed value then `$render()` is called to update the input control. + * * The value referenced by `ng-model` is changed programmatically and both the `$modelValue` and + * the `$viewValue` are different to last time. + * + * Since `ng-model` does not do a deep watch, `$render()` is only invoked if the values of + * `$modelValue` and `$viewValue` are actually different to their previous value. If `$modelValue` + * or `$viewValue` are objects (rather than a string or number) then `$render()` will not be + * invoked if you only change a property on the objects. */ this.$render = noop; @@ -17511,63 +19163,44 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ }; var parentForm = $element.inheritedData('$formController') || nullFormCtrl, - invalidCount = 0, // used to easily determine if we are valid - $error = this.$error = {}; // keep invalid keys here - + currentValidationRunId = 0; // Setup initial state of the control - $element.addClass(PRISTINE_CLASS); - toggleValidCss(true); - - // convenience method for easy toggling of classes - function toggleValidCss(isValid, validationErrorKey) { - validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : ''; - $animate.removeClass($element, (isValid ? INVALID_CLASS : VALID_CLASS) + validationErrorKey); - $animate.addClass($element, (isValid ? VALID_CLASS : INVALID_CLASS) + validationErrorKey); - } + $element + .addClass(PRISTINE_CLASS) + .addClass(UNTOUCHED_CLASS); /** * @ngdoc method * @name ngModel.NgModelController#$setValidity * * @description - * Change the validity state, and notifies the form when the control changes validity. (i.e. it - * does not notify form if given validator is already marked as invalid). + * Change the validity state, and notifies the form. * - * This method should be called by validators - i.e. the parser or formatter functions. + * This method can be called within $parsers/$formatters. However, if possible, please use the + * `ngModel.$validators` pipeline which is designed to call this method automatically. * * @param {string} validationErrorKey Name of the validator. the `validationErrorKey` will assign - * to `$error[validationErrorKey]=!isValid` so that it is available for data-binding. + * to `$error[validationErrorKey]` and `$pending[validationErrorKey]` + * so that it is available for data-binding. * The `validationErrorKey` should be in camelCase and will get converted into dash-case * for class name. Example: `myError` will result in `ng-valid-my-error` and `ng-invalid-my-error` * class and can be bound to as `{{someForm.someControl.$error.myError}}` . - * @param {boolean} isValid Whether the current state is valid (true) or invalid (false). + * @param {boolean} isValid Whether the current state is valid (true), invalid (false), pending (undefined), + * or skipped (null). */ - this.$setValidity = function(validationErrorKey, isValid) { - // Purposeful use of ! here to cast isValid to boolean in case it is undefined - // jshint -W018 - if ($error[validationErrorKey] === !isValid) return; - // jshint +W018 - - if (isValid) { - if ($error[validationErrorKey]) invalidCount--; - if (!invalidCount) { - toggleValidCss(true); - this.$valid = true; - this.$invalid = false; - } - } else { - toggleValidCss(false); - this.$invalid = true; - this.$valid = false; - invalidCount++; - } - - $error[validationErrorKey] = !isValid; - toggleValidCss(isValid, validationErrorKey); - - parentForm.$setValidity(validationErrorKey, isValid, this); - }; + addSetValidityMethod({ + ctrl: this, + $element: $element, + set: function(object, property) { + object[property] = true; + }, + unset: function(object, property) { + delete object[property]; + }, + parentForm: parentForm, + $animate: $animate + }); /** * @ngdoc method @@ -17577,89 +19210,408 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * Sets the control to its pristine state. * * This method can be called to remove the 'ng-dirty' class and set the control to its pristine - * state (ng-pristine class). + * state (ng-pristine class). A model is considered to be pristine when the model has not been changed + * from when first compiled within then form. */ this.$setPristine = function () { - this.$dirty = false; - this.$pristine = true; + ctrl.$dirty = false; + ctrl.$pristine = true; $animate.removeClass($element, DIRTY_CLASS); $animate.addClass($element, PRISTINE_CLASS); }; /** * @ngdoc method - * @name ngModel.NgModelController#$setViewValue + * @name ngModel.NgModelController#$setUntouched * * @description - * Update the view value. + * Sets the control to its untouched state. * - * This method should be called when the view value changes, typically from within a DOM event handler. - * For example {@link ng.directive:input input} and - * {@link ng.directive:select select} directives call it. + * This method can be called to remove the 'ng-touched' class and set the control to its + * untouched state (ng-untouched class). Upon compilation, a model is set as untouched + * by default, however this function can be used to restore that state if the model has + * already been touched by the user. + */ + this.$setUntouched = function() { + ctrl.$touched = false; + ctrl.$untouched = true; + $animate.setClass($element, UNTOUCHED_CLASS, TOUCHED_CLASS); + }; + + /** + * @ngdoc method + * @name ngModel.NgModelController#$setTouched * - * It will update the $viewValue, then pass this value through each of the functions in `$parsers`, - * which includes any validators. The value that comes out of this `$parsers` pipeline, be applied to - * `$modelValue` and the **expression** specified in the `ng-model` attribute. + * @description + * Sets the control to its touched state. * - * Lastly, all the registered change listeners, in the `$viewChangeListeners` list, are called. + * This method can be called to remove the 'ng-untouched' class and set the control to its + * touched state (ng-touched class). A model is considered to be touched when the user has + * first interacted (focussed) on the model input element and then shifted focus away (blurred) + * from the input element. + */ + this.$setTouched = function() { + ctrl.$touched = true; + ctrl.$untouched = false; + $animate.setClass($element, TOUCHED_CLASS, UNTOUCHED_CLASS); + }; + + /** + * @ngdoc method + * @name ngModel.NgModelController#$rollbackViewValue * - * Note that calling this function does not trigger a `$digest`. + * @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 + * future event. * - * @param {string} value Value from the view. + * If you have an input that uses `ng-model-options` to set up debounced events or events such + * as blur you can have a situation where there is a period when the `$viewValue` + * is out of synch with the ngModel's `$modelValue`. + * + * In this case, you can run into difficulties if you try to update the ngModel's `$modelValue` + * programmatically before these debounced/future events have resolved/occurred, because Angular's + * dirty checking mechanism is not able to tell whether the model has actually changed or not. + * + * The `$rollbackViewValue()` method should be called before programmatically changing the model of an + * input which may have such events pending. This is important in order to make sure that the + * input field will be updated with the new model value and any pending operations are cancelled. + * + * <example name="ng-model-cancel-update" module="cancel-update-example"> + * <file name="app.js"> + * angular.module('cancel-update-example', []) + * + * .controller('CancelUpdateController', ['$scope', function($scope) { + * $scope.resetWithCancel = function (e) { + * if (e.keyCode == 27) { + * $scope.myForm.myInput1.$rollbackViewValue(); + * $scope.myValue = ''; + * } + * }; + * $scope.resetWithoutCancel = function (e) { + * if (e.keyCode == 27) { + * $scope.myValue = ''; + * } + * }; + * }]); + * </file> + * <file name="index.html"> + * <div ng-controller="CancelUpdateController"> + * <p>Try typing something in each input. See that the model only updates when you + * blur off the input. + * </p> + * <p>Now see what happens if you start typing then press the Escape key</p> + * + * <form name="myForm" ng-model-options="{ updateOn: 'blur' }"> + * <p>With $rollbackViewValue()</p> + * <input name="myInput1" ng-model="myValue" ng-keydown="resetWithCancel($event)"><br/> + * myValue: "{{ myValue }}" + * + * <p>Without $rollbackViewValue()</p> + * <input name="myInput2" ng-model="myValue" ng-keydown="resetWithoutCancel($event)"><br/> + * myValue: "{{ myValue }}" + * </form> + * </div> + * </file> + * </example> + */ + this.$rollbackViewValue = function() { + $timeout.cancel(pendingDebounce); + ctrl.$viewValue = ctrl.$$lastCommittedViewValue; + ctrl.$render(); + }; + + /** + * @ngdoc method + * @name ngModel.NgModelController#$validate + * + * @description + * Runs each of the registered validators (first synchronous validators and then asynchronous validators). */ - this.$setViewValue = function(value) { - this.$viewValue = value; + this.$validate = function() { + // ignore $validate before model is initialized + if (isNumber(ctrl.$modelValue) && isNaN(ctrl.$modelValue)) { + return; + } + this.$$parseAndValidate(); + }; + + this.$$runValidators = function(parseValid, modelValue, viewValue, doneCallback) { + currentValidationRunId++; + var localValidationRunId = currentValidationRunId; + + // check parser error + if (!processParseErrors(parseValid)) { + return; + } + if (!processSyncValidators()) { + return; + } + processAsyncValidators(); + + function processParseErrors(parseValid) { + var errorKey = ctrl.$$parserName || 'parse'; + if (parseValid === undefined) { + setValidity(errorKey, null); + } else { + setValidity(errorKey, parseValid); + if (!parseValid) { + forEach(ctrl.$validators, function(v, name) { + setValidity(name, null); + }); + forEach(ctrl.$asyncValidators, function(v, name) { + setValidity(name, null); + }); + validationDone(); + return false; + } + } + return true; + } + + function processSyncValidators() { + var syncValidatorsValid = true; + forEach(ctrl.$validators, function(validator, name) { + var result = validator(modelValue, viewValue); + syncValidatorsValid = syncValidatorsValid && result; + setValidity(name, result); + }); + if (!syncValidatorsValid) { + forEach(ctrl.$asyncValidators, function(v, name) { + setValidity(name, null); + }); + validationDone(); + return false; + } + return true; + } + + function processAsyncValidators() { + var validatorPromises = []; + forEach(ctrl.$asyncValidators, function(validator, name) { + var promise = validator(modelValue, viewValue); + if (!isPromiseLike(promise)) { + throw $ngModelMinErr("$asyncValidators", + "Expected asynchronous validator to return a promise but got '{0}' instead.", promise); + } + setValidity(name, undefined); + validatorPromises.push(promise.then(function() { + setValidity(name, true); + }, function(error) { + setValidity(name, false); + })); + }); + if (!validatorPromises.length) { + validationDone(); + } else { + $q.all(validatorPromises).then(validationDone); + } + } + + function setValidity(name, isValid) { + if (localValidationRunId === currentValidationRunId) { + ctrl.$setValidity(name, isValid); + } + } + + function validationDone() { + if (localValidationRunId === currentValidationRunId) { + + doneCallback(); + } + } + }; + + /** + * @ngdoc method + * @name ngModel.NgModelController#$commitViewValue + * + * @description + * Commit a pending update to the `$modelValue`. + * + * Updates may be pending by a debounced event or because the input is waiting for a some future + * event defined in `ng-model-options`. this method is rarely needed as `NgModelController` + * usually handles calling this in response to input events. + */ + this.$commitViewValue = function() { + var viewValue = ctrl.$viewValue; + + $timeout.cancel(pendingDebounce); + + // If the view value has not changed then we should just exit, except in the case where there is + // a native validator on the element. In this case the validation state may have changed even though + // the viewValue has stayed empty. + if (ctrl.$$lastCommittedViewValue === viewValue && (viewValue !== '' || !ctrl.$$hasNativeValidators)) { + return; + } + ctrl.$$lastCommittedViewValue = viewValue; // change to dirty - if (this.$pristine) { - this.$dirty = true; - this.$pristine = false; + if (ctrl.$pristine) { + ctrl.$dirty = true; + ctrl.$pristine = false; $animate.removeClass($element, PRISTINE_CLASS); $animate.addClass($element, DIRTY_CLASS); parentForm.$setDirty(); } + this.$$parseAndValidate(); + }; - forEach(this.$parsers, function(fn) { - value = fn(value); + this.$$parseAndValidate = function() { + var parserValid = true, + viewValue = ctrl.$$lastCommittedViewValue, + modelValue = viewValue; + for(var i = 0; i < ctrl.$parsers.length; i++) { + modelValue = ctrl.$parsers[i](modelValue); + if (isUndefined(modelValue)) { + parserValid = false; + break; + } + } + if (isNumber(ctrl.$modelValue) && isNaN(ctrl.$modelValue)) { + // ctrl.$modelValue has not been touched yet... + ctrl.$modelValue = ngModelGet(); + } + var prevModelValue = ctrl.$modelValue; + var allowInvalid = ctrl.$options && ctrl.$options.allowInvalid; + if (allowInvalid) { + ctrl.$modelValue = modelValue; + writeToModelIfNeeded(); + } + ctrl.$$runValidators(parserValid, modelValue, viewValue, function() { + if (!allowInvalid) { + ctrl.$modelValue = ctrl.$valid ? modelValue : undefined; + writeToModelIfNeeded(); + } }); - if (this.$modelValue !== value) { - this.$modelValue = value; - ngModelSet($scope, value); - forEach(this.$viewChangeListeners, function(listener) { - try { - listener(); - } catch(e) { - $exceptionHandler(e); - } + function writeToModelIfNeeded() { + if (ctrl.$modelValue !== prevModelValue) { + ctrl.$$writeModelToScope(); + } + } + }; + + this.$$writeModelToScope = function() { + ngModelSet(ctrl.$modelValue); + forEach(ctrl.$viewChangeListeners, function(listener) { + try { + listener(); + } catch(e) { + $exceptionHandler(e); + } + }); + }; + + /** + * @ngdoc method + * @name ngModel.NgModelController#$setViewValue + * + * @description + * Update the view value. + * + * This method should be called when an input directive want to change the view value; typically, + * this is done from within a DOM event handler. + * + * For example {@link ng.directive:input input} calls it when the value of the input changes and + * {@link ng.directive:select select} calls it when an option is selected. + * + * If the new `value` is an object (rather than a string or a number), we should make a copy of the + * object before passing it to `$setViewValue`. This is because `ngModel` does not perform a deep + * watch of objects, it only looks for a change of identity. If you only change the property of + * the object then ngModel will not realise that the object has changed and will not invoke the + * `$parsers` and `$validators` pipelines. + * + * For this reason, you should not change properties of the copy once it has been passed to + * `$setViewValue`. Otherwise you may cause the model value on the scope to change incorrectly. + * + * When this method is called, the new `value` will be staged for committing through the `$parsers` + * and `$validators` pipelines. If there are no special {@link ngModelOptions} specified then the staged + * value sent directly for processing, finally to be applied to `$modelValue` and then the + * **expression** specified in the `ng-model` attribute. + * + * Lastly, all the registered change listeners, in the `$viewChangeListeners` list, are called. + * + * In case the {@link ng.directive:ngModelOptions ngModelOptions} directive is used with `updateOn` + * and the `default` trigger is not listed, all those actions will remain pending until one of the + * `updateOn` events is triggered on the DOM element. + * All these actions will be debounced if the {@link ng.directive:ngModelOptions ngModelOptions} + * directive is used with a custom debounce for this particular event. + * + * Note that calling this function does not trigger a `$digest`. + * + * @param {string} value Value from the view. + * @param {string} trigger Event that triggered the update. + */ + this.$setViewValue = function(value, trigger) { + ctrl.$viewValue = value; + if (!ctrl.$options || ctrl.$options.updateOnDefault) { + ctrl.$$debounceViewValueCommit(trigger); + } + }; + + this.$$debounceViewValueCommit = function(trigger) { + var debounceDelay = 0, + options = ctrl.$options, + debounce; + + if (options && isDefined(options.debounce)) { + debounce = options.debounce; + if (isNumber(debounce)) { + debounceDelay = debounce; + } else if (isNumber(debounce[trigger])) { + debounceDelay = debounce[trigger]; + } else if (isNumber(debounce['default'])) { + debounceDelay = debounce['default']; + } + } + + $timeout.cancel(pendingDebounce); + if (debounceDelay) { + pendingDebounce = $timeout(function() { + ctrl.$commitViewValue(); + }, debounceDelay); + } else if ($rootScope.$$phase) { + ctrl.$commitViewValue(); + } else { + $scope.$apply(function() { + ctrl.$commitViewValue(); }); } }; // model -> value - var ctrl = this; - + // Note: we cannot use a normal scope.$watch as we want to detect the following: + // 1. scope value is 'a' + // 2. user enters 'b' + // 3. ng-change kicks in and reverts scope value to 'a' + // -> scope value did not change since the last digest as + // ng-change executes in apply phase + // 4. view should be changed back to 'a' $scope.$watch(function ngModelWatch() { - var value = ngModelGet($scope); + var modelValue = ngModelGet(); // if scope model value and ngModel value are out of sync - if (ctrl.$modelValue !== value) { + // TODO(perf): why not move this to the action fn? + if (modelValue !== ctrl.$modelValue) { + ctrl.$modelValue = modelValue; var formatters = ctrl.$formatters, idx = formatters.length; - ctrl.$modelValue = value; + var viewValue = modelValue; while(idx--) { - value = formatters[idx](value); + viewValue = formatters[idx](viewValue); } - - if (ctrl.$viewValue !== value) { - ctrl.$viewValue = value; + if (ctrl.$viewValue !== viewValue) { + ctrl.$viewValue = ctrl.$$lastCommittedViewValue = viewValue; ctrl.$render(); + + ctrl.$$runValidators(undefined, modelValue, viewValue, noop); } } - return value; + return modelValue; }); }]; @@ -17680,8 +19632,8 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * - Binding the view into the model, which other directives such as `input`, `textarea` or `select` * require. * - Providing validation behavior (i.e. required, number, email, url). - * - Keeping the state of the control (valid/invalid, dirty/pristine, validation errors). - * - Setting related css classes on the element (`ng-valid`, `ng-invalid`, `ng-dirty`, `ng-pristine`) including animations. + * - Keeping the state of the control (valid/invalid, dirty/pristine, touched/untouched, validation errors). + * - Setting related css classes on the element (`ng-valid`, `ng-invalid`, `ng-dirty`, `ng-pristine`, `ng-touched`, `ng-untouched`) including animations. * - Registering the control with its parent {@link ng.directive:form form}. * * Note: `ngModel` will try to bind to the property given by evaluating the expression on the @@ -17701,6 +19653,11 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * - {@link input[number] number} * - {@link input[email] email} * - {@link input[url] url} + * - {@link input[date] date} + * - {@link input[dateTimeLocal] dateTimeLocal} + * - {@link input[time] time} + * - {@link input[month] month} + * - {@link input[week] week} * - {@link ng.directive:select select} * - {@link ng.directive:textarea textarea} * @@ -17766,22 +19723,91 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ </form> </file> * </example> + * + * ## Binding to a getter/setter + * + * Sometimes it's helpful to bind `ngModel` to a getter/setter function. A getter/setter is a + * function that returns a representation of the model when called with zero arguments, and sets + * the internal state of a model when called with an argument. It's sometimes useful to use this + * for models that have an internal representation that's different than what the model exposes + * to the view. + * + * <div class="alert alert-success"> + * **Best Practice:** It's best to keep getters fast because Angular is likely to call them more + * frequently than other parts of your code. + * </div> + * + * You use this behavior by adding `ng-model-options="{ getterSetter: true }"` to an element that + * has `ng-model` attached to it. You can also add `ng-model-options="{ getterSetter: true }"` to + * a `<form>`, which will enable this behavior for all `<input>`s within it. See + * {@link ng.directive:ngModelOptions `ngModelOptions`} for more. + * + * The following example shows how to use `ngModel` with a getter/setter: + * + * @example + * <example name="ngModel-getter-setter" module="getterSetterExample"> + <file name="index.html"> + <div ng-controller="ExampleController"> + <form name="userForm"> + Name: + <input type="text" name="userName" + ng-model="user.name" + ng-model-options="{ getterSetter: true }" /> + </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) { + if (angular.isDefined(newName)) { + _name = newName; + } + return _name; + } + }; + }]); + </file> + * </example> */ var ngModelDirective = function() { return { - require: ['ngModel', '^?form'], + restrict: 'A', + require: ['ngModel', '^?form', '^?ngModelOptions'], controller: NgModelController, - link: function(scope, element, attr, ctrls) { - // notify others, especially parent forms + link: { + pre: function(scope, element, attr, ctrls) { + var modelCtrl = ctrls[0], + formCtrl = ctrls[1] || nullFormCtrl; - var modelCtrl = ctrls[0], - formCtrl = ctrls[1] || nullFormCtrl; + modelCtrl.$$setOptions(ctrls[2] && ctrls[2].$options); - formCtrl.$addControl(modelCtrl); + // notify others, especially parent forms + formCtrl.$addControl(modelCtrl); - scope.$on('$destroy', function() { - formCtrl.$removeControl(modelCtrl); - }); + scope.$on('$destroy', function() { + formCtrl.$removeControl(modelCtrl); + }); + }, + post: function(scope, element, attr, ctrls) { + var modelCtrl = ctrls[0]; + if (modelCtrl.$options && modelCtrl.$options.updateOn) { + element.on(modelCtrl.$options.updateOn, function(ev) { + modelCtrl.$$debounceViewValueCommit(ev && ev.type); + }); + } + + element.on('blur', function(ev) { + if (modelCtrl.$touched) return; + + scope.$apply(function() { + modelCtrl.$setTouched(); + }); + }); + } } }; }; @@ -17796,7 +19822,15 @@ var ngModelDirective = function() { * The expression is evaluated immediately, unlike the JavaScript onchange event * which only triggers at the end of a change (usually, when the user leaves the * form element or presses the return key). - * The expression is not evaluated when the value change is coming from the model. + * + * The `ngChange` expression is only evaluated when a change in the input value causes + * a new value to be committed to the model. + * + * It will not be evaluated: + * * if the value returned from the `$parsers` transformation pipeline has not changed + * * if the input has continued to be invalid since the model will stay `null` + * * if the model is changed programmatically and not by a change to the input value + * * * Note, this directive requires `ngModel` to be present. * @@ -17847,6 +19881,7 @@ var ngModelDirective = function() { * </example> */ var ngChangeDirective = valueFn({ + restrict: 'A', require: 'ngModel', link: function(scope, element, attr, ctrl) { ctrl.$viewChangeListeners.push(function() { @@ -17858,27 +19893,89 @@ var ngChangeDirective = valueFn({ var requiredDirective = function() { return { + restrict: 'A', require: '?ngModel', link: function(scope, elm, attr, ctrl) { if (!ctrl) return; attr.required = true; // force truthy in case we are on non input element - var validator = function(value) { - if (attr.required && ctrl.$isEmpty(value)) { - ctrl.$setValidity('required', false); - return; - } else { - ctrl.$setValidity('required', true); - return value; + ctrl.$validators.required = function(modelValue, viewValue) { + return !attr.required || !ctrl.$isEmpty(viewValue); + }; + + attr.$observe('required', function() { + ctrl.$validate(); + }); + } + }; +}; + + +var patternDirective = function() { + return { + restrict: 'A', + require: '?ngModel', + link: function(scope, elm, attr, ctrl) { + if (!ctrl) return; + + var regexp, patternExp = attr.ngPattern || attr.pattern; + attr.$observe('pattern', function(regex) { + if (isString(regex) && regex.length > 0) { + regex = new RegExp(regex); } + + if (regex && !regex.test) { + throw minErr('ngPattern')('noregexp', + 'Expected {0} to be a RegExp but was {1}. Element: {2}', patternExp, + regex, startingTag(elm)); + } + + regexp = regex || undefined; + ctrl.$validate(); + }); + + ctrl.$validators.pattern = function(value) { + return ctrl.$isEmpty(value) || isUndefined(regexp) || regexp.test(value); }; + } + }; +}; - ctrl.$formatters.push(validator); - ctrl.$parsers.unshift(validator); - attr.$observe('required', function() { - validator(ctrl.$viewValue); +var maxlengthDirective = function() { + return { + restrict: 'A', + require: '?ngModel', + link: function(scope, elm, attr, ctrl) { + if (!ctrl) return; + + var maxlength = 0; + attr.$observe('maxlength', function(value) { + maxlength = int(value) || 0; + ctrl.$validate(); + }); + ctrl.$validators.maxlength = function(modelValue, viewValue) { + return ctrl.$isEmpty(viewValue) || viewValue.length <= maxlength; + }; + } + }; +}; + +var minlengthDirective = function() { + return { + restrict: 'A', + require: '?ngModel', + link: function(scope, elm, attr, ctrl) { + if (!ctrl) return; + + var minlength = 0; + attr.$observe('minlength', function(value) { + minlength = int(value) || 0; + ctrl.$validate(); }); + ctrl.$validators.minlength = function(modelValue, viewValue) { + return ctrl.$isEmpty(viewValue) || viewValue.length >= minlength; + }; } }; }; @@ -17889,62 +19986,94 @@ var requiredDirective = function() { * @name ngList * * @description - * Text input that converts between a delimited string and an array of strings. The delimiter - * can be a fixed string (by default a comma) or a regular expression. + * Text input that converts between a delimited string and an array of strings. The default + * delimiter is a comma followed by a space - equivalent to `ng-list=", "`. You can specify a custom + * delimiter as the value of the `ngList` attribute - for example, `ng-list=" | "`. + * + * The behaviour of the directive is affected by the use of the `ngTrim` attribute. + * * If `ngTrim` is set to `"false"` then whitespace around both the separator and each + * list item is respected. This implies that the user of the directive is responsible for + * dealing with whitespace but also allows you to use whitespace as a delimiter, such as a + * tab or newline character. + * * Otherwise whitespace around the delimiter is ignored when splitting (although it is respected + * when joining the list items back together) and whitespace around each list item is stripped + * before it is added to the model. + * + * ### Example with Validation + * + * <example name="ngList-directive" module="listExample"> + * <file name="app.js"> + * angular.module('listExample', []) + * .controller('ExampleController', ['$scope', function($scope) { + * $scope.names = ['morpheus', 'neo', 'trinity']; + * }]); + * </file> + * <file name="index.html"> + * <form name="myForm" ng-controller="ExampleController"> + * List: <input name="namesInput" ng-model="names" ng-list required> + * <span class="error" ng-show="myForm.namesInput.$error.required"> + * Required!</span> + * <br> + * <tt>names = {{names}}</tt><br/> + * <tt>myForm.namesInput.$valid = {{myForm.namesInput.$valid}}</tt><br/> + * <tt>myForm.namesInput.$error = {{myForm.namesInput.$error}}</tt><br/> + * <tt>myForm.$valid = {{myForm.$valid}}</tt><br/> + * <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/> + * </form> + * </file> + * <file name="protractor.js" type="protractor"> + * var listInput = element(by.model('names')); + * var names = element(by.exactBinding('names')); + * var valid = element(by.binding('myForm.namesInput.$valid')); + * var error = element(by.css('span.error')); + * + * it('should initialize to model', function() { + * expect(names.getText()).toContain('["morpheus","neo","trinity"]'); + * expect(valid.getText()).toContain('true'); + * expect(error.getCssValue('display')).toBe('none'); + * }); * - * @element input - * @param {string=} ngList optional delimiter that should be used to split the value. If - * specified in form `/something/` then the value will be converted into a regular expression. + * it('should be invalid if empty', function() { + * listInput.clear(); + * listInput.sendKeys(''); * - * @example - <example name="ngList-directive" module="listExample"> - <file name="index.html"> - <script> - angular.module('listExample', []) - .controller('ExampleController', ['$scope', function($scope) { - $scope.names = ['igor', 'misko', 'vojta']; - }]); - </script> - <form name="myForm" ng-controller="ExampleController"> - List: <input name="namesInput" ng-model="names" ng-list required> - <span class="error" ng-show="myForm.namesInput.$error.required"> - Required!</span> - <br> - <tt>names = {{names}}</tt><br/> - <tt>myForm.namesInput.$valid = {{myForm.namesInput.$valid}}</tt><br/> - <tt>myForm.namesInput.$error = {{myForm.namesInput.$error}}</tt><br/> - <tt>myForm.$valid = {{myForm.$valid}}</tt><br/> - <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/> - </form> - </file> - <file name="protractor.js" type="protractor"> - var listInput = element(by.model('names')); - var names = element(by.binding('{{names}}')); - var valid = element(by.binding('myForm.namesInput.$valid')); - var error = element(by.css('span.error')); - - it('should initialize to model', function() { - expect(names.getText()).toContain('["igor","misko","vojta"]'); - expect(valid.getText()).toContain('true'); - expect(error.getCssValue('display')).toBe('none'); - }); - - it('should be invalid if empty', function() { - listInput.clear(); - listInput.sendKeys(''); - - expect(names.getText()).toContain(''); - expect(valid.getText()).toContain('false'); - expect(error.getCssValue('display')).not.toBe('none'); }); - </file> - </example> + * expect(names.getText()).toContain(''); + * expect(valid.getText()).toContain('false'); + * expect(error.getCssValue('display')).not.toBe('none'); + * }); + * </file> + * </example> + * + * ### Example - splitting on whitespace + * <example name="ngList-directive-newlines"> + * <file name="index.html"> + * <textarea ng-model="list" ng-list=" " ng-trim="false"></textarea> + * <pre>{{ list | json }}</pre> + * </file> + * <file name="protractor.js" type="protractor"> + * it("should split the text by newlines", function() { + * var listInput = element(by.model('list')); + * var output = element(by.binding('list | json')); + * listInput.sendKeys('abc\ndef\nghi'); + * expect(output.getText()).toContain('[\n "abc",\n "def",\n "ghi"\n]'); + * }); + * </file> + * </example> + * + * @element input + * @param {string=} ngList optional delimiter that should be used to split the value. */ var ngListDirective = function() { return { + restrict: 'A', + priority: 100, require: 'ngModel', link: function(scope, element, attr, ctrl) { - var match = /\/(.*)\//.exec(attr.ngList), - separator = match && new RegExp(match[1]) || attr.ngList || ','; + // We want to control whitespace trimming so we use this convoluted approach + // to access the ngList attribute, which doesn't pre-trim the attribute + var ngList = element.attr(attr.$attr.ngList) || ', '; + var trimValues = attr.ngTrim !== 'false'; + var separator = trimValues ? trim(ngList) : ngList; var parse = function(viewValue) { // If the viewValue is invalid (say required but empty) it will be `undefined` @@ -17954,7 +20083,7 @@ var ngListDirective = function() { if (viewValue) { forEach(viewValue.split(separator), function(value) { - if (value) list.push(trim(value)); + if (value) list.push(trimValues ? trim(value) : value); }); } @@ -17964,7 +20093,7 @@ var ngListDirective = function() { ctrl.$parsers.push(parse); ctrl.$formatters.push(function(value) { if (isArray(value)) { - return value.join(', '); + return value.join(ngList); } return undefined; @@ -18034,6 +20163,7 @@ var CONSTANT_VALUE_REGEXP = /^(true|false|\d+)$/; */ var ngValueDirective = function() { return { + restrict: 'A', priority: 100, compile: function(tpl, tplAttr) { if (CONSTANT_VALUE_REGEXP.test(tplAttr.ngValue)) { @@ -18053,6 +20183,278 @@ var ngValueDirective = function() { /** * @ngdoc directive + * @name ngModelOptions + * + * @description + * Allows tuning how model updates are done. Using `ngModelOptions` you can specify a custom list of + * events that will trigger a model update and/or a debouncing delay so that the actual update only + * takes place when a timer expires; this timer will be reset after another change takes place. + * + * Given the nature of `ngModelOptions`, the value displayed inside input fields in the view might + * be different than the value in the actual model. This means that if you update the model you + * should also invoke {@link ngModel.NgModelController `$rollbackViewValue`} on the relevant input field in + * order to make sure it is synchronized with the model and that any debounced action is canceled. + * + * The easiest way to reference the control's {@link ngModel.NgModelController `$rollbackViewValue`} + * method is by making sure the input is placed inside a form that has a `name` attribute. This is + * important because `form` controllers are published to the related scope under the name in their + * `name` attribute. + * + * Any pending changes will take place immediately when an enclosing form is submitted via the + * `submit` event. Note that `ngClick` events will occur before the model is updated. Use `ngSubmit` + * to have access to the updated model. + * + * @param {Object} ngModelOptions options to apply to the current model. Valid keys are: + * - `updateOn`: string specifying which event should be the input bound to. You can set several + * events using an space delimited list. There is a special event called `default` that + * matches the default events belonging of the control. + * - `debounce`: integer value which contains the debounce model update value in milliseconds. A + * value of 0 triggers an immediate update. If an object is supplied instead, you can specify a + * custom value for each event. For example: + * `ng-model-options="{ updateOn: 'default blur', debounce: {'default': 500, 'blur': 0} }"` + * - `allowInvalid`: boolean value which indicates that the model can be set with values that did + * not validate correctly instead of the default behavior of setting the model to undefined. + * - `getterSetter`: boolean value which determines whether or not to treat functions bound to + `ngModel` as getters/setters. + * - `timezone`: Defines the timezone to be used to read/write the `Date` instance in the model for + * `<input type="date">`, `<input type="time">`, ... . Right now, the only supported value is `'UTC'`, + * otherwise the default 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"> + Name: + <input type="text" name="userName" + ng-model="user.name" + ng-model-options="{ updateOn: 'blur' }" + ng-keyup="cancel($event)" /><br /> + + Other data: + <input type="text" ng-model="user.data" /><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> + + 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"> + 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> + + 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"> + Name: + <input type="text" name="userName" + ng-model="user.name" + ng-model-options="{ getterSetter: true }" /> + </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> + */ +var ngModelOptionsDirective = function() { + return { + restrict: 'A', + controller: ['$scope', '$attrs', function($scope, $attrs) { + var that = this; + this.$options = $scope.$eval($attrs.ngModelOptions); + // Allow adding/overriding bound events + if (this.$options.updateOn !== undefined) { + 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, + parentForm = context.parentForm, + $animate = context.$animate; + + ctrl.$setValidity = setValidity; + toggleValidationCss('', true); + + function setValidity(validationErrorKey, state, options) { + if (state === undefined) { + createAndSet('$pending', validationErrorKey, options); + } else { + unsetAndCleanup('$pending', validationErrorKey, options); + } + if (!isBoolean(state)) { + unset(ctrl.$error, validationErrorKey, options); + unset(ctrl.$$success, validationErrorKey, options); + } else { + if (state) { + unset(ctrl.$error, validationErrorKey, options); + set(ctrl.$$success, validationErrorKey, options); + } else { + set(ctrl.$error, validationErrorKey, options); + unset(ctrl.$$success, validationErrorKey, options); + } + } + 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); + parentForm.$setValidity(validationErrorKey, combinedState, ctrl); + } + + function createAndSet(name, value, options) { + if (!ctrl[name]) { + ctrl[name] = {}; + } + set(ctrl[name], value, options); + } + + function unsetAndCleanup(name, value, options) { + if (ctrl[name]) { + unset(ctrl[name], value, options); + } + if (isObjectEmpty(ctrl[name])) { + ctrl[name] = undefined; + } + } + + function cachedToggleClass(className, switchValue) { + if (switchValue && !classCache[className]) { + $animate.addClass($element, className); + classCache[className] = true; + } else if (!switchValue && classCache[className]) { + $animate.removeClass($element, className); + classCache[className] = false; + } + } + + function toggleValidationCss(validationErrorKey, isValid) { + validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : ''; + + cachedToggleClass(VALID_CLASS + validationErrorKey, isValid === true); + cachedToggleClass(INVALID_CLASS + validationErrorKey, isValid === false); + } +} + +function isObjectEmpty(obj) { + if (obj) { + for (var prop in obj) { + return false; + } + } + return true; +} + +/** + * @ngdoc directive * @name ngBind * @restrict AC * @@ -18064,7 +20466,7 @@ var ngValueDirective = function() { * Typically, you don't use `ngBind` directly, but instead you use the double curly markup like * `{{ expression }}` which is similar but less verbose. * - * It is preferable to use `ngBind` instead of `{{ expression }}` when a template is momentarily + * It is preferable to use `ngBind` instead of `{{ expression }}` if a template is momentarily * displayed by the browser in its raw state before Angular compiles it. Since `ngBind` is an * element attribute, it makes the bindings invisible to the user while the page is loading. * @@ -18102,20 +20504,23 @@ var ngValueDirective = function() { </file> </example> */ -var ngBindDirective = ngDirective({ - compile: function(templateElement) { - templateElement.addClass('ng-binding'); - return function (scope, element, attr) { - element.data('$binding', attr.ngBind); - scope.$watch(attr.ngBind, function ngBindWatchAction(value) { - // We are purposefully using == here rather than === because we want to - // catch when value is "null or undefined" - // jshint -W041 - element.text(value == undefined ? '' : value); - }); - }; - } -}); +var ngBindDirective = ['$compile', function($compile) { + return { + restrict: 'AC', + compile: function ngBindCompile(templateElement) { + $compile.$$addBindingClass(templateElement); + return function ngBindLink(scope, element, attr) { + $compile.$$addBindingInfo(element, attr.ngBind); + scope.$watch(attr.ngBind, function ngBindWatchAction(value) { + // We are purposefully using == here rather than === because we want to + // catch when value is "null or undefined" + // jshint -W041 + element.text(value == undefined ? '' : value); + }); + }; + } + }; +}]; /** @@ -18169,14 +20574,18 @@ var ngBindDirective = ngDirective({ </file> </example> */ -var ngBindTemplateDirective = ['$interpolate', function($interpolate) { - return function(scope, element, attr) { - // TODO: move this to scenario runner - var interpolateFn = $interpolate(element.attr(attr.$attr.ngBindTemplate)); - element.addClass('ng-binding').data('$binding', interpolateFn); - attr.$observe('ngBindTemplate', function(value) { - element.text(value); - }); +var ngBindTemplateDirective = ['$interpolate', '$compile', function($interpolate, $compile) { + return { + compile: function ngBindTemplateCompile(templateElement) { + $compile.$$addBindingClass(templateElement); + return function ngBindTemplateLink(scope, element, attr) { + var interpolateFn = $interpolate(element.attr(attr.$attr.ngBindTemplate)); + $compile.$$addBindingInfo(element, interpolateFn.expressions); + attr.$observe('ngBindTemplate', function(value) { + element.text(value); + }); + }; + } }; }]; @@ -18201,7 +20610,6 @@ var ngBindTemplateDirective = ['$interpolate', function($interpolate) { * @param {expression} ngBindHtml {@link guide/expression Expression} to evaluate. * * @example - Try it here: enter text in text box and watch the greeting change. <example module="bindHtmlExample" deps="angular-sanitize.js"> <file name="index.html"> @@ -18227,16 +20635,26 @@ var ngBindTemplateDirective = ['$interpolate', function($interpolate) { </file> </example> */ -var ngBindHtmlDirective = ['$sce', '$parse', function($sce, $parse) { - return function(scope, element, attr) { - element.addClass('ng-binding').data('$binding', attr.ngBindHtml); +var ngBindHtmlDirective = ['$sce', '$parse', '$compile', function($sce, $parse, $compile) { + return { + restrict: 'A', + compile: function ngBindHtmlCompile(tElement, tAttrs) { + var ngBindHtmlGetter = $parse(tAttrs.ngBindHtml); + var ngBindHtmlWatch = $parse(tAttrs.ngBindHtml, function getStringValue(value) { + return (value || '').toString(); + }); + $compile.$$addBindingClass(tElement); - var parsed = $parse(attr.ngBindHtml); - function getStringValue() { return (parsed(scope) || '').toString(); } + return function ngBindHtmlLink(scope, element, attr) { + $compile.$$addBindingInfo(element, attr.ngBindHtml); - scope.$watch(getStringValue, function ngBindHtmlWatchAction(value) { - element.html($sce.getTrustedHtml(parsed(scope)) || ''); - }); + scope.$watch(ngBindHtmlWatch, function ngBindHtmlWatchAction() { + // we re-evaluate the expr because we want a TrustedValueHolderType + // for $sce, not a string + element.html($sce.getTrustedHtml(ngBindHtmlGetter(scope)) || ''); + }); + }; + } }; }]; @@ -18296,15 +20714,13 @@ function classDirective(name, selector) { function updateClasses (oldClasses, newClasses) { var toAdd = arrayDifference(newClasses, oldClasses); var toRemove = arrayDifference(oldClasses, newClasses); - toRemove = digestClassCounts(toRemove, -1); toAdd = digestClassCounts(toAdd, 1); - - if (toAdd.length === 0) { - $animate.removeClass(element, toRemove); - } else if (toRemove.length === 0) { + toRemove = digestClassCounts(toRemove, -1); + if (toAdd && toAdd.length) { $animate.addClass(element, toAdd); - } else { - $animate.setClass(element, toAdd, toRemove); + } + if (toRemove && toRemove.length) { + $animate.removeClass(element, toRemove); } } @@ -18684,10 +21100,16 @@ var ngCloakDirective = ngDirective({ * * @element ANY * @scope - * @param {expression} ngController Name of a globally accessible constructor function or an - * {@link guide/expression expression} that on the current scope evaluates to a - * constructor function. The controller instance can be published into a scope property - * by specifying `as propertyName`. + * @param {expression} ngController Name of a constructor function registered with the current + * {@link ng.$controllerProvider $controllerProvider} or an {@link guide/expression expression} + * that on the current scope evaluates to a constructor function. + * + * The controller instance can be published into a scope property by specifying + * `ng-controller="as propertyName"`. + * + * If the current `$controllerProvider` is configured to use globals (via + * {@link ng.$controllerProvider#allowGlobals `$controllerProvider.allowGlobals()` }), this may + * also be the name of a globally accessible constructor function (not recommended). * * @example * Here is a simple form for editing user contact information. Adding, removing, clearing, and @@ -18882,6 +21304,7 @@ var ngCloakDirective = ngDirective({ */ var ngControllerDirective = [function() { return { + restrict: 'A', scope: true, controller: '@', priority: 500 @@ -18899,8 +21322,10 @@ var ngControllerDirective = [function() { * This is necessary when developing things like Google Chrome Extensions. * * CSP forbids apps to use `eval` or `Function(string)` generated functions (among other things). - * For us to be compatible, we just need to implement the "getterFn" in $parse without violating - * any of these restrictions. + * For Angular to be CSP compatible there are only two things that we need to do differently: + * + * - don't use `Function` constructor to generate optimized value getters + * - don't inject custom stylesheet into the document * * AngularJS uses `Function(string)` generated functions as a speed optimization. Applying the `ngCsp` * directive will cause Angular to use CSP compatibility mode. When this mode is on AngularJS will @@ -18911,7 +21336,18 @@ var ngControllerDirective = [function() { * includes some CSS rules (e.g. {@link ng.directive:ngCloak ngCloak}). * To make those directives work in CSP mode, include the `angular-csp.css` manually. * - * In order to use this feature put the `ngCsp` directive on the root element of the application. + * Angular tries to autodetect if CSP is active and automatically turn on the CSP-safe mode. 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 + * script in the following Content Security Policy directive: "default-src 'self'". Note that + * 'script-src' was not explicitly set, so 'default-src' is used as a fallback. + * ``` + * + * This error is harmless but annoying. To prevent the error from showing up, put the `ngCsp` + * directive on the root element of the application or on the `angular.js` script tag, whichever + * appears first in the html document. * * *Note: This directive is only available in the `ng-csp` and `data-ng-csp` attribute form.* * @@ -18926,9 +21362,9 @@ var ngControllerDirective = [function() { ``` */ -// 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 a csp() fn that looks for 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 system (before $parse is instantiated), for this reason we just have +// the csp.isActive() fn that looks for ng-csp attribute anywhere in the current doc /** * @ngdoc directive @@ -18949,7 +21385,9 @@ var ngControllerDirective = [function() { <button ng-click="count = count + 1" ng-init="count=0"> Increment </button> - count: {{count}} + <span> + count: {{count}} + </span> </file> <file name="protractor.js" type="protractor"> it('should check ng-click', function() { @@ -18967,19 +21405,34 @@ var ngControllerDirective = [function() { * Events that are handled via these handler are always configured not to propagate further. */ var ngEventDirectives = {}; + +// For events that might fire synchronously during DOM manipulation +// we need to execute their event handlers asynchronously using $evalAsync, +// so that they are not executed in an inconsistent state. +var forceAsyncEvents = { + 'blur': true, + 'focus': true +}; forEach( 'click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste'.split(' '), function(name) { var directiveName = directiveNormalize('ng-' + name); - ngEventDirectives[directiveName] = ['$parse', function($parse) { + ngEventDirectives[directiveName] = ['$parse', '$rootScope', function($parse, $rootScope) { return { + restrict: 'A', compile: function($element, attr) { var fn = $parse(attr[directiveName]); return function ngEventHandler(scope, element) { - element.on(lowercase(name), function(event) { - scope.$apply(function() { + var eventName = lowercase(name); + element.on(eventName, function(event) { + var callback = function() { fn(scope, {$event:event}); - }); + }; + if (forceAsyncEvents[eventName] && $rootScope.$$phase) { + scope.$evalAsync(callback); + } else { + scope.$apply(callback); + } }); }; } @@ -19237,6 +21690,13 @@ forEach( * server and reloading the current page), but only if the form does not contain `action`, * `data-action`, or `x-action` attributes. * + * <div class="alert alert-warning"> + * **Warning:** Be careful not to cause "double-submission" by using both the `ngClick` and + * `ngSubmit` handlers together. See the + * {@link form#submitting-a-form-and-preventing-the-default-action `form` directive documentation} + * for a detailed discussion of when `ngSubmit` may be triggered. + * </div> + * * @element form * @priority 0 * @param {expression} ngSubmit {@link guide/expression Expression} to eval. @@ -19289,6 +21749,10 @@ forEach( * @description * Specify custom behavior on focus event. * + * Note: As the `focus` event is executed synchronously when calling `input.focus()` + * AngularJS executes the expression using `scope.$evalAsync` if the event is fired + * during an `$apply` to ensure a consistent state. + * * @element window, input, select, textarea, a * @priority 0 * @param {expression} ngFocus {@link guide/expression Expression} to evaluate upon @@ -19305,6 +21769,14 @@ forEach( * @description * Specify custom behavior on blur event. * + * A [blur event](https://developer.mozilla.org/en-US/docs/Web/Events/blur) fires when + * an element has lost focus. + * + * Note: As the `blur` event is executed synchronously also during DOM manipulations + * (e.g. removing a focussed input), + * AngularJS executes the expression using `scope.$evalAsync` if the event is fired + * during an `$apply` to ensure a consistent state. + * * @element window, input, select, textarea, a * @priority 0 * @param {expression} ngBlur {@link guide/expression Expression} to evaluate upon @@ -19455,6 +21927,7 @@ forEach( */ var ngIfDirective = ['$animate', function($animate) { return { + multiElement: true, transclude: 'element', priority: 600, terminal: true, @@ -19464,10 +21937,10 @@ var ngIfDirective = ['$animate', function($animate) { var block, childScope, previousElements; $scope.$watch($attr.ngIf, function ngIfWatchAction(value) { - if (toBoolean(value)) { + if (value) { if (!childScope) { - childScope = $scope.$new(); - $transclude(childScope, function (clone) { + $transclude(function (clone, newScope) { + childScope = newScope; clone[clone.length++] = document.createComment(' end ngIf: ' + $attr.ngIf + ' '); // Note: We only need the first/last node of the cloned nodes. // However, we need to keep the reference to the jqlite wrapper as it might be changed later @@ -19488,8 +21961,8 @@ var ngIfDirective = ['$animate', function($animate) { childScope = null; } if(block) { - previousElements = getBlockElements(block.clone); - $animate.leave(previousElements, function() { + previousElements = getBlockNodes(block.clone); + $animate.leave(previousElements).then(function() { previousElements = null; }); block = null; @@ -19660,8 +22133,17 @@ var ngIfDirective = ['$animate', function($animate) { * @description * Emitted every time the ngInclude content is reloaded. */ -var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$animate', '$sce', - function($http, $templateCache, $anchorScroll, $animate, $sce) { + + +/** + * @ngdoc event + * @name ngInclude#$includeContentError + * @eventType emit on the scope ngInclude was declared in + * @description + * Emitted when a template HTTP request yields an erronous response (status < 200 || status > 299) + */ +var ngIncludeDirective = ['$templateRequest', '$anchorScroll', '$animate', '$sce', + function($templateRequest, $anchorScroll, $animate, $sce) { return { restrict: 'ECA', priority: 400, @@ -19689,7 +22171,7 @@ var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$animate' currentScope = null; } if(currentElement) { - $animate.leave(currentElement, function() { + $animate.leave(currentElement).then(function() { previousElement = null; }); previousElement = currentElement; @@ -19706,7 +22188,9 @@ var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$animate' var thisChangeId = ++changeCounter; if (src) { - $http.get(src, {cache: $templateCache}).success(function(response) { + //set the 2nd param to true to ignore the template request error so that the inner + //contents and scope can be cleaned up. + $templateRequest(src, true).then(function(response) { if (thisChangeId !== changeCounter) return; var newScope = scope.$new(); ctrl.template = response; @@ -19719,7 +22203,7 @@ var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$animate' // directives to non existing elements. var clone = $transclude(newScope, function(clone) { cleanupLastIncludeContent(); - $animate.enter(clone, null, $element, afterAnimation); + $animate.enter(clone, null, $element).then(afterAnimation); }); currentScope = newScope; @@ -19727,8 +22211,11 @@ var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$animate' currentScope.$emit('$includeContentLoaded'); scope.$eval(onloadExp); - }).error(function() { - if (thisChangeId === changeCounter) cleanupLastIncludeContent(); + }, function() { + if (thisChangeId === changeCounter) { + cleanupLastIncludeContent(); + scope.$emit('$includeContentError'); + } }); scope.$emit('$includeContentRequested'); } else { @@ -19753,6 +22240,18 @@ var ngIncludeFillContentDirective = ['$compile', priority: -400, require: 'ngInclude', link: function(scope, $element, $attr, ctrl) { + if (/SVG/.test($element[0].toString())) { + // WebKit: https://bugs.webkit.org/show_bug.cgi?id=135698 --- SVG elements do not + // support innerHTML, so detect this here and try to generate the contents + // specially. + $element.empty(); + $compile(jqLiteBuildFragment(ctrl.template, document).childNodes)(scope, + function namespaceAdaptedClone(clone) { + $element.append(clone); + }, undefined, undefined, $element); + return; + } + $element.html(ctrl.template); $compile($element.contents())(scope); } @@ -20065,7 +22564,7 @@ var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interp //if explicit number rule such as 1, 2, 3... is defined, just use it. Otherwise, //check it against pluralization rules in $locale service if (!(value in whens)) value = $locale.pluralCat(value - offset); - return whensExpFns[value](scope, element, true); + return whensExpFns[value](scope); } else { return ''; } @@ -20176,6 +22675,13 @@ var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interp * For example: `item in items` is equivalent to `item in items track by $id(item)`. This implies that the DOM elements * will be associated by item identity in the array. * + * * `variable in expression as alias_expression` – You can also provide an optional alias expression which will then store the + * intermediate results of the repeater after the filters have been applied. Typically this is used to render a special message + * when a filter is active on the repeater, but the filtered result set is empty. + * + * For example: `item in items | filter:x as results` will store the fragment of the repeated items as `results`, but only after + * the items have been processed through the filter. + * * For example: `item in items track by $id(item)`. A built in `$id()` function can be used to assign a unique * `$$hashKey` property to each item in the array. This property is then used as a key to associated DOM elements * with the corresponding item in the array by identity. Moving the same object in array would move the DOM @@ -20208,9 +22714,12 @@ var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interp I have {{friends.length}} friends. They are: <input type="search" ng-model="q" placeholder="filter friends..." /> <ul class="example-animate-container"> - <li class="animate-repeat" ng-repeat="friend in friends | filter:q"> + <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"> + <strong>No results found...</strong> + </li> </ul> </div> </file> @@ -20277,29 +22786,84 @@ var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interp var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { var NG_REMOVED = '$$NG_REMOVED'; var ngRepeatMinErr = minErr('ngRepeat'); + + var updateScope = function(scope, index, valueIdentifier, value, keyIdentifier, key, arrayLength) { + // TODO(perf): generate setters to shave off ~40ms or 1-1.5% + scope[valueIdentifier] = value; + if (keyIdentifier) scope[keyIdentifier] = key; + scope.$index = index; + scope.$first = (index === 0); + scope.$last = (index === (arrayLength - 1)); + scope.$middle = !(scope.$first || scope.$last); + // jshint bitwise: false + scope.$odd = !(scope.$even = (index&1) === 0); + // jshint bitwise: true + }; + + var getBlockStart = function(block) { + return block.clone[0]; + }; + + var getBlockEnd = function(block) { + return block.clone[block.clone.length - 1]; + }; + + return { + restrict: 'A', + multiElement: true, transclude: 'element', priority: 1000, terminal: true, $$tlb: true, - link: function($scope, $element, $attr, ctrl, $transclude){ - var expression = $attr.ngRepeat; - var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?\s*$/), - trackByExp, trackByExpGetter, trackByIdExpFn, trackByIdArrayFn, trackByIdObjFn, - lhs, rhs, valueIdentifier, keyIdentifier, - hashFnLocals = {$id: hashKey}; - - if (!match) { - throw ngRepeatMinErr('iexp', "Expected expression in form of '_item_ in _collection_[ track by _id_]' but got '{0}'.", + compile: function ngRepeatCompile($element, $attr) { + var expression = $attr.ngRepeat; + var ngRepeatEndComment = document.createComment(' end ngRepeat: ' + expression + ' '); + + var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+track\s+by\s+([\s\S]+?))?\s*$/); + + if (!match) { + throw ngRepeatMinErr('iexp', "Expected expression in form of '_item_ in _collection_[ track by _id_]' but got '{0}'.", expression); - } + } + + var lhs = match[1]; + var rhs = match[2]; + var aliasAs = match[3]; + var trackByExp = match[4]; + + match = lhs.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/); + + if (!match) { + throw ngRepeatMinErr('iidexp', "'_item_' in '_item_ in _collection_' should be an identifier or '(_key_, _value_)' expression, but got '{0}'.", + lhs); + } + var valueIdentifier = match[3] || match[1]; + var keyIdentifier = match[2]; + + if (aliasAs && (!/^[$a-zA-Z_][$a-zA-Z0-9_]*$/.test(aliasAs) || + /^(null|undefined|this|\$index|\$first|\$middle|\$last|\$even|\$odd|\$parent)$/.test(aliasAs))) { + throw ngRepeatMinErr('badident', "alias '{0}' is invalid --- must be a valid JS identifier which is not a reserved name.", + aliasAs); + } - lhs = match[1]; - rhs = match[2]; - trackByExp = match[3]; + var trackByExpGetter, trackByIdExpFn, trackByIdArrayFn, trackByIdObjFn; + var hashFnLocals = {$id: hashKey}; - if (trackByExp) { - trackByExpGetter = $parse(trackByExp); + if (trackByExp) { + trackByExpGetter = $parse(trackByExp); + } else { + trackByIdArrayFn = function (key, value) { + return hashKey(value); + }; + trackByIdObjFn = function (key) { + return key; + }; + } + + return function ngRepeatLink($scope, $element, $attr, ctrl, $transclude) { + + if (trackByExpGetter) { trackByIdExpFn = function(key, value, index) { // assign key, value, and $index to the locals so that they can be used in hash functions if (keyIdentifier) hashFnLocals[keyIdentifier] = key; @@ -20307,48 +22871,39 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { hashFnLocals.$index = index; return trackByExpGetter($scope, hashFnLocals); }; - } else { - trackByIdArrayFn = function(key, value) { - return hashKey(value); - }; - trackByIdObjFn = function(key) { - return key; - }; - } - - match = lhs.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/); - if (!match) { - throw ngRepeatMinErr('iidexp', "'_item_' in '_item_ in _collection_' should be an identifier or '(_key_, _value_)' expression, but got '{0}'.", - lhs); } - valueIdentifier = match[3] || match[1]; - keyIdentifier = match[2]; // Store a list of elements from previous run. This is a hash where key is the item from the // iterator, and the value is objects with following properties. // - scope: bound scope // - element: previous element. // - index: position - var lastBlockMap = {}; + // + // We are using no-proto object so that we don't need to guard against inherited props via + // hasOwnProperty. + var lastBlockMap = createMap(); //watch props - $scope.$watchCollection(rhs, function ngRepeatAction(collection){ + $scope.$watchCollection(rhs, function ngRepeatAction(collection) { var index, length, - previousNode = $element[0], // current position of the node + previousNode = $element[0], // node that cloned nodes should be inserted after + // initialized to the comment node anchor nextNode, // Same as lastBlockMap but it has the current state. It will become the // lastBlockMap on the next iteration. - nextBlockMap = {}, - arrayLength, - childScope, + nextBlockMap = createMap(), + collectionLength, key, value, // key/value of iteration trackById, trackByIdFn, collectionKeys, block, // last object information {scope, element, id} - nextBlockOrder = [], + nextBlockOrder, elementsToRemove; + if (aliasAs) { + $scope[aliasAs] = collection; + } if (isArrayLike(collection)) { collectionKeys = collection; @@ -20357,118 +22912,106 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { trackByIdFn = trackByIdExpFn || trackByIdObjFn; // if object, extract keys, sort them and use to determine order of iteration over obj props collectionKeys = []; - for (key in collection) { - if (collection.hasOwnProperty(key) && key.charAt(0) != '$') { - collectionKeys.push(key); + for (var itemKey in collection) { + if (collection.hasOwnProperty(itemKey) && itemKey.charAt(0) != '$') { + collectionKeys.push(itemKey); } } collectionKeys.sort(); } - arrayLength = collectionKeys.length; + collectionLength = collectionKeys.length; + nextBlockOrder = new Array(collectionLength); // locate existing items - length = nextBlockOrder.length = collectionKeys.length; - for(index = 0; index < length; index++) { - key = (collection === collectionKeys) ? index : collectionKeys[index]; - value = collection[key]; - trackById = trackByIdFn(key, value, index); - assertNotHasOwnProperty(trackById, '`track by` id'); - if(lastBlockMap.hasOwnProperty(trackById)) { - block = lastBlockMap[trackById]; - delete lastBlockMap[trackById]; - nextBlockMap[trackById] = block; - nextBlockOrder[index] = block; - } else if (nextBlockMap.hasOwnProperty(trackById)) { - // restore lastBlockMap - forEach(nextBlockOrder, function(block) { - if (block && block.scope) lastBlockMap[block.id] = block; - }); - // This is a duplicate and we need to throw an error - throw ngRepeatMinErr('dupes', "Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: {0}, Duplicate key: {1}", - expression, trackById); - } else { - // new never before seen block - nextBlockOrder[index] = { id: trackById }; - nextBlockMap[trackById] = false; - } - } + for (index = 0; index < collectionLength; index++) { + key = (collection === collectionKeys) ? index : collectionKeys[index]; + value = collection[key]; + trackById = trackByIdFn(key, value, index); + if (lastBlockMap[trackById]) { + // found previously seen block + block = lastBlockMap[trackById]; + delete lastBlockMap[trackById]; + nextBlockMap[trackById] = block; + nextBlockOrder[index] = block; + } else if (nextBlockMap[trackById]) { + // if collision detected. restore lastBlockMap and throw an error + forEach(nextBlockOrder, function (block) { + if (block && block.scope) lastBlockMap[block.id] = block; + }); + throw ngRepeatMinErr('dupes', + "Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: {0}, Duplicate key: {1}, Duplicate value: {2}", + expression, trackById, toJson(value)); + } else { + // new never before seen block + nextBlockOrder[index] = {id: trackById, scope: undefined, clone: undefined}; + nextBlockMap[trackById] = true; + } + } - // remove existing items - for (key in lastBlockMap) { - // lastBlockMap is our own object so we don't need to use special hasOwnPropertyFn - if (lastBlockMap.hasOwnProperty(key)) { - block = lastBlockMap[key]; - elementsToRemove = getBlockElements(block.clone); - $animate.leave(elementsToRemove); - forEach(elementsToRemove, function(element) { element[NG_REMOVED] = true; }); - block.scope.$destroy(); + // remove leftover items + for (var blockKey in lastBlockMap) { + block = lastBlockMap[blockKey]; + elementsToRemove = getBlockNodes(block.clone); + $animate.leave(elementsToRemove); + if (elementsToRemove[0].parentNode) { + // if the element was not removed yet because of pending animation, mark it as deleted + // so that we can ignore it later + for (index = 0, length = elementsToRemove.length; index < length; index++) { + elementsToRemove[index][NG_REMOVED] = true; + } } + block.scope.$destroy(); } // we are not using forEach for perf reasons (trying to avoid #call) - for (index = 0, length = collectionKeys.length; index < length; index++) { + for (index = 0; index < collectionLength; index++) { key = (collection === collectionKeys) ? index : collectionKeys[index]; value = collection[key]; block = nextBlockOrder[index]; - if (nextBlockOrder[index - 1]) previousNode = getBlockEnd(nextBlockOrder[index - 1]); if (block.scope) { // if we have already seen this object, then we need to reuse the // associated scope/element - childScope = block.scope; nextNode = previousNode; + + // skip nodes that are already pending removal via leave animation do { nextNode = nextNode.nextSibling; - } while(nextNode && nextNode[NG_REMOVED]); + } while (nextNode && nextNode[NG_REMOVED]); if (getBlockStart(block) != nextNode) { // existing item which got moved - $animate.move(getBlockElements(block.clone), null, jqLite(previousNode)); + $animate.move(getBlockNodes(block.clone), null, jqLite(previousNode)); } previousNode = getBlockEnd(block); + updateScope(block.scope, index, valueIdentifier, value, keyIdentifier, key, collectionLength); } else { // new item which we don't know about - childScope = $scope.$new(); - } + $transclude(function ngRepeatTransclude(clone, scope) { + block.scope = scope; + // http://jsperf.com/clone-vs-createcomment + var endNode = ngRepeatEndComment.cloneNode(false); + clone[clone.length++] = endNode; - childScope[valueIdentifier] = value; - if (keyIdentifier) childScope[keyIdentifier] = key; - childScope.$index = index; - childScope.$first = (index === 0); - childScope.$last = (index === (arrayLength - 1)); - childScope.$middle = !(childScope.$first || childScope.$last); - // jshint bitwise: false - childScope.$odd = !(childScope.$even = (index&1) === 0); - // jshint bitwise: true - - if (!block.scope) { - $transclude(childScope, function(clone) { - clone[clone.length++] = document.createComment(' end ngRepeat: ' + expression + ' '); + // TODO(perf): support naked previousNode in `enter` to avoid creation of jqLite wrapper? $animate.enter(clone, null, jqLite(previousNode)); - previousNode = clone; - block.scope = childScope; + previousNode = endNode; // Note: We only need the first/last node of the cloned nodes. // However, we need to keep the reference to the jqlite wrapper as it might be changed later // by a directive with templateUrl when its template arrives. block.clone = clone; nextBlockMap[block.id] = block; + updateScope(block.scope, index, valueIdentifier, value, keyIdentifier, key, collectionLength); }); } } lastBlockMap = nextBlockMap; }); + }; } }; - - function getBlockStart(block) { - return block.clone[0]; - } - - function getBlockEnd(block) { - return block.clone[block.clone.length - 1]; - } }]; /** @@ -20490,15 +23033,10 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { * <div ng-show="myValue" class="ng-hide"></div> * ``` * - * When the ngShow expression evaluates to false then the ng-hide CSS class is added to the class attribute - * on the element causing it to become hidden. When true, the ng-hide CSS class is removed + * 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. * - * <div class="alert alert-warning"> - * **Note:** Here is a list of values that ngShow will consider as a falsy value (case insensitive):<br /> - * "f" / "0" / "false" / "no" / "n" / "[]" - * </div> - * * ## 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 @@ -20518,7 +23056,7 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { * * ```css * .ng-hide { - * //this is just another form of hiding an element + * /* this is just another form of hiding an element */ * display:block!important; * position:absolute; * top:-9999px; @@ -20540,7 +23078,15 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { * //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; + * /* this is required as of 1.3x to properly + * apply all styling in a show/hide animation */ + * transition:0s linear all; + * } + * + * .my-element.ng-hide-add-active, + * .my-element.ng-hide-remove-active { + * /* the transition is defined in the active class */ + * transition:1s linear all; * } * * .my-element.ng-hide-add { ... } @@ -20549,7 +23095,7 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { * .my-element.ng-hide-remove.ng-hide-remove-active { ... } * ``` * - * Keep in mind that, as of AngularJS version 1.2.17 (and 1.3.0-beta.11), there is no need to change the display + * Keep in mind that, as of AngularJS version 1.3.0-beta.11, there is no need to change the display * property to block during animation states--ngAnimate will handle the style toggling automatically for you. * * @animations @@ -20582,8 +23128,6 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { </file> <file name="animations.css"> .animate-show { - -webkit-transition:all linear 0.5s; - transition:all linear 0.5s; line-height:20px; opacity:1; padding:10px; @@ -20591,6 +23135,12 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { background:white; } + .animate-show.ng-hide-add.ng-hide-add-active, + .animate-show.ng-hide-remove.ng-hide-remove-active { + -webkit-transition:all linear 0.5s; + transition:all linear 0.5s; + } + .animate-show.ng-hide { line-height:0; opacity:0; @@ -20620,10 +23170,14 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { </example> */ var ngShowDirective = ['$animate', function($animate) { - return function(scope, element, attr) { - scope.$watch(attr.ngShow, function ngShowWatchAction(value){ - $animate[toBoolean(value) ? 'removeClass' : 'addClass'](element, 'ng-hide'); - }); + return { + restrict: 'A', + multiElement: true, + link: function(scope, element, attr) { + scope.$watch(attr.ngShow, function ngShowWatchAction(value){ + $animate[value ? 'removeClass' : 'addClass'](element, 'ng-hide'); + }); + } }; }]; @@ -20647,15 +23201,10 @@ var ngShowDirective = ['$animate', function($animate) { * <div ng-hide="myValue"></div> * ``` * - * When the ngHide expression evaluates to true then the .ng-hide CSS class is added to the class attribute - * on the element causing it to become hidden. When false, the ng-hide CSS class is removed + * 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. * - * <div class="alert alert-warning"> - * **Note:** Here is a list of values that ngHide will consider as a falsy value (case insensitive):<br /> - * "f" / "0" / "false" / "no" / "n" / "[]" - * </div> - * * ## 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 @@ -20675,7 +23224,7 @@ var ngShowDirective = ['$animate', function($animate) { * * ```css * .ng-hide { - * //this is just another form of hiding an element + * /* this is just another form of hiding an element */ * display:block!important; * position:absolute; * top:-9999px; @@ -20705,7 +23254,7 @@ var ngShowDirective = ['$animate', function($animate) { * .my-element.ng-hide-remove.ng-hide-remove-active { ... } * ``` * - * Keep in mind that, as of AngularJS version 1.2.17 (and 1.3.0-beta.11), there is no need to change the display + * Keep in mind that, as of AngularJS version 1.3.0-beta.11, there is no need to change the display * property to block during animation states--ngAnimate will handle the style toggling automatically for you. * * @animations @@ -20776,10 +23325,14 @@ var ngShowDirective = ['$animate', function($animate) { </example> */ var ngHideDirective = ['$animate', function($animate) { - return function(scope, element, attr) { - scope.$watch(attr.ngHide, function ngHideWatchAction(value){ - $animate[toBoolean(value) ? 'addClass' : 'removeClass'](element, 'ng-hide'); - }); + return { + restrict: 'A', + multiElement: true, + link: function(scope, element, attr) { + scope.$watch(attr.ngHide, function ngHideWatchAction(value){ + $animate[value ? 'addClass' : 'removeClass'](element, 'ng-hide'); + }); + } }; }]; @@ -20880,7 +23433,7 @@ var ngStyleDirective = ngDirective(function(scope, element, attr) { * * * @scope - * @priority 800 + * @priority 1200 * @param {*} ngSwitch|on expression to match against <tt>ng-switch-when</tt>. * On child elements add: * @@ -20979,37 +23532,39 @@ var ngSwitchDirective = ['$animate', function($animate) { var watchExpr = attr.ngSwitch || attr.on, selectedTranscludes = [], selectedElements = [], - previousElements = [], + previousLeaveAnimations = [], selectedScopes = []; + var spliceFactory = function(array, index) { + return function() { array.splice(index, 1); }; + }; + scope.$watch(watchExpr, function ngSwitchWatchAction(value) { var i, ii; - for (i = 0, ii = previousElements.length; i < ii; ++i) { - previousElements[i].remove(); + for (i = 0, ii = previousLeaveAnimations.length; i < ii; ++i) { + $animate.cancel(previousLeaveAnimations[i]); } - previousElements.length = 0; + previousLeaveAnimations.length = 0; for (i = 0, ii = selectedScopes.length; i < ii; ++i) { - var selected = selectedElements[i]; + var selected = getBlockNodes(selectedElements[i].clone); selectedScopes[i].$destroy(); - previousElements[i] = selected; - $animate.leave(selected, function() { - previousElements.splice(i, 1); - }); + var promise = previousLeaveAnimations[i] = $animate.leave(selected); + promise.then(spliceFactory(previousLeaveAnimations, i)); } selectedElements.length = 0; selectedScopes.length = 0; if ((selectedTranscludes = ngSwitchController.cases['!' + value] || ngSwitchController.cases['?'])) { - scope.$eval(attr.change); forEach(selectedTranscludes, function(selectedTransclude) { - var selectedScope = scope.$new(); - selectedScopes.push(selectedScope); - selectedTransclude.transclude(selectedScope, function(caseElement) { + selectedTransclude.transclude(function(caseElement, selectedScope) { + selectedScopes.push(selectedScope); var anchor = selectedTransclude.element; + caseElement[caseElement.length++] = document.createComment(' end ngSwitchWhen: '); + var block = { clone: caseElement }; - selectedElements.push(caseElement); + selectedElements.push(block); $animate.enter(caseElement, anchor.parent(), anchor); }); }); @@ -21021,8 +23576,9 @@ var ngSwitchDirective = ['$animate', function($animate) { var ngSwitchWhenDirective = ngDirective({ transclude: 'element', - priority: 800, + priority: 1200, 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 }); @@ -21031,8 +23587,9 @@ var ngSwitchWhenDirective = ngDirective({ var ngSwitchDefaultDirective = ngDirective({ transclude: 'element', - priority: 800, + priority: 1200, require: '^ngSwitch', + multiElement: true, link: function(scope, element, attr, ctrl, $transclude) { ctrl.cases['?'] = (ctrl.cases['?'] || []); ctrl.cases['?'].push({ transclude: $transclude, element: element }); @@ -21042,7 +23599,7 @@ var ngSwitchDefaultDirective = ngDirective({ /** * @ngdoc directive * @name ngTransclude - * @restrict AC + * @restrict EAC * * @description * Directive that marks the insertion point for the transcluded DOM of the nearest parent directive that uses transclusion. @@ -21063,7 +23620,7 @@ var ngSwitchDefaultDirective = ngDirective({ scope: { title:'@' }, template: '<div style="border: 1px solid black;">' + '<div style="background-color: gray">{{title}}</div>' + - '<div ng-transclude></div>' + + '<ng-transclude></ng-transclude>' + '</div>' }; }) @@ -21094,6 +23651,7 @@ var ngSwitchDefaultDirective = ngDirective({ * */ var ngTranscludeDirective = ngDirective({ + restrict: 'EAC', link: function($scope, $element, $attrs, controller, $transclude) { if (!$transclude) { throw minErr('ngTransclude')('orphan', @@ -21274,7 +23832,7 @@ var ngOptionsMinErr = minErr('ngOptions'); Select <a href ng-click="myColor = { name:'not in list', shade: 'other' }">bogus</a>.<br> <hr/> - Currently selected: {{ {selected_color:myColor} }} + Currently selected: {{ {selected_color:myColor} }} <div style="border:solid 1px black; height:20px" ng-style="{'background-color':myColor.name}"> </div> @@ -21294,7 +23852,11 @@ var ngOptionsMinErr = minErr('ngOptions'); </example> */ -var ngOptionsDirective = valueFn({ terminal: true }); +var ngOptionsDirective = valueFn({ + restrict: 'A', + terminal: true +}); + // jshint maxlen: false var selectDirective = ['$compile', '$parse', function($compile, $parse) { //000011111111110000000000022222222220000000000000000000003333333333000000000000004444444444444440000000005555555555555550000000666666666666666000000000000000777777777700000000000000000008888888888 @@ -21323,7 +23885,7 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { }; - self.addOption = function(value) { + self.addOption = function(value, element) { assertNotHasOwnProperty(value, '"option value"'); optionsMap[value] = true; @@ -21331,6 +23893,12 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { $element.val(value); if (unknownOption.parent()) unknownOption.remove(); } + // 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 (element[0].hasAttribute('selected')) { + element[0].selected = true; + } }; @@ -21373,6 +23941,7 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { optionsExp = attr.ngOptions, nullOption = false, // if false, user will not be able to select it (used by ngOptions) emptyOption, + renderScheduled = false, // we can't just jqLite('<option>') since jqLite is not smart enough // to create it in <select> and IE barfs otherwise. optionTemplate = jqLite(document.createElement('option')), @@ -21554,23 +24123,51 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { value = valueFn(scope, locals); } } - // Update the null option's selected property here so $render cleans it up correctly - if (optionGroupsCache[0].length > 1) { - if (optionGroupsCache[0][1].id !== key) { - optionGroupsCache[0][1].selected = false; - } - } } ctrl.$setViewValue(value); + render(); }); }); ctrl.$render = render; - // TODO(vojta): can't we optimize this ? - scope.$watch(render); + scope.$watchCollection(valuesFn, scheduleRendering); + + if (multiple) { + scope.$watchCollection(function() { return ctrl.$modelValue; }, scheduleRendering); + } + + + function getSelectedSet() { + var selectedSet = false; + if (multiple) { + var modelValue = ctrl.$modelValue; + if (trackFn && isArray(modelValue)) { + selectedSet = new HashMap([]); + var locals = {}; + for (var trackIndex = 0; trackIndex < modelValue.length; trackIndex++) { + locals[valueName] = modelValue[trackIndex]; + selectedSet.put(trackFn(scope, locals), modelValue[trackIndex]); + } + } else { + selectedSet = new HashMap(modelValue); + } + } + return selectedSet; + } + + + function scheduleRendering() { + if (!renderScheduled) { + scope.$$postDigest(render); + renderScheduled = true; + } + } + function render() { + renderScheduled = false; + // Temporary location for the option groups before we render them var optionGroups = {'':[]}, optionGroupNames = [''], @@ -21586,22 +24183,11 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { groupIndex, index, locals = {}, selected, - selectedSet = false, // nothing is selected yet + selectedSet = getSelectedSet(), lastElement, element, label; - if (multiple) { - if (trackFn && isArray(modelValue)) { - selectedSet = new HashMap([]); - for (var trackIndex = 0; trackIndex < modelValue.length; trackIndex++) { - locals[valueName] = modelValue[trackIndex]; - selectedSet.put(trackFn(scope, locals), modelValue[trackIndex]); - } - } else { - selectedSet = new HashMap(modelValue); - } - } // We now build up the list of options we need (we merge later) for (index = 0; length = keys.length, index < length; index++) { @@ -21697,8 +24283,14 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { lastElement.val(existingOption.id = option.id); } // lastElement.prop('selected') provided by jQuery has side-effects - if (existingOption.selected !== option.selected) { + if (lastElement[0].selected !== option.selected) { lastElement.prop('selected', (existingOption.selected = option.selected)); + if (msie) { + // See #7692 + // The selected item wouldn't visually update on IE without this. + // Tested on Win7: IE9, IE10 and IE11. Future IEs should be tested as well + lastElement.prop('selected', existingOption.selected); + } } } else { // grow elements @@ -21714,6 +24306,7 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { (element = optionTemplate.clone()) .val(option.id) .prop('selected', option.selected) + .attr('selected', option.selected) .text(option.label); } @@ -21781,11 +24374,13 @@ var optionDirective = ['$interpolate', function($interpolate) { if (interpolateFn) { scope.$watch(interpolateFn, function interpolateWatchAction(newVal, oldVal) { attr.$set('value', newVal); - if (newVal !== oldVal) selectCtrl.removeOption(oldVal); - selectCtrl.addOption(newVal); + if (oldVal !== newVal) { + selectCtrl.removeOption(oldVal); + } + selectCtrl.addOption(newVal, element); }); } else { - selectCtrl.addOption(attr.value); + selectCtrl.addOption(attr.value, element); } element.on('$destroy', function() { @@ -21798,7 +24393,7 @@ var optionDirective = ['$interpolate', function($interpolate) { var styleDirective = valueFn({ restrict: 'E', - terminal: true + terminal: false }); if (window.angular.bootstrap) { @@ -21807,7 +24402,7 @@ var styleDirective = valueFn({ return; } - //try to bind to jquery now so that one can write angular.element().read() + //try to bind to jquery now so that one can write jqLite(document).ready() //but we will rebind on bootstrap again. bindJQuery(); @@ -21819,4 +24414,4 @@ var styleDirective = valueFn({ })(window, document); -!window.angular.$$csp() && window.angular.element(document).find('head').prepend('<style type="text/css">@charset "UTF-8";[ng\\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak,.ng-hide{display:none !important;}ng\\:form{display:block;}.ng-animate-block-transitions{transition:0s all!important;-webkit-transition:0s all!important;}.ng-hide-add-active,.ng-hide-remove{display:block!important;}</style>'); \ No newline at end of file +!window.angular.$$csp() && window.angular.element(document).find('head').prepend('<style type="text/css">@charset "UTF-8";[ng\\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak,.ng-hide:not(.ng-animate){display:none !important;}ng\\:form{display:block;}</style>'); \ No newline at end of file diff --git a/webclient/js/angular.min.js b/webclient/js/angular.min.js index 1456067c2f..5475589e2f 100644 --- a/webclient/js/angular.min.js +++ b/webclient/js/angular.min.js @@ -1,200 +1,237 @@ /* - AngularJS v1.2.0 - (c) 2010-2012 Google, Inc. http://angularjs.org + AngularJS v1.3.0-rc.1 + (c) 2010-2014 Google, Inc. http://angularjs.org License: MIT */ -(function(W,O,s){'use strict';function L(b){return function(){var a=arguments[0],c,a="["+(b?b+":":"")+a+"] http://errors.angularjs.org/undefined/"+(b?b+"/":"")+a;for(c=1;c<arguments.length;c++)a=a+(1==c?"?":"&")+"p"+(c-1)+"="+encodeURIComponent("function"==typeof arguments[c]?arguments[c].toString().replace(/ \{[\s\S]*$/,""):"undefined"==typeof arguments[c]?"undefined":"string"!=typeof arguments[c]?JSON.stringify(arguments[c]):arguments[c]);return Error(a)}}function pb(b){if(null==b||ya(b))return!1; -var a=b.length;return 1===b.nodeType&&a?!0:D(b)||J(b)||0===a||"number"===typeof a&&0<a&&a-1 in b}function q(b,a,c){var d;if(b)if(B(b))for(d in b)"prototype"!=d&&("length"!=d&&"name"!=d&&b.hasOwnProperty(d))&&a.call(c,b[d],d);else if(b.forEach&&b.forEach!==q)b.forEach(a,c);else if(pb(b))for(d=0;d<b.length;d++)a.call(c,b[d],d);else for(d in b)b.hasOwnProperty(d)&&a.call(c,b[d],d);return b}function Nb(b){var a=[],c;for(c in b)b.hasOwnProperty(c)&&a.push(c);return a.sort()}function Jc(b,a,c){for(var d= -Nb(b),e=0;e<d.length;e++)a.call(c,b[d[e]],d[e]);return d}function Ob(b){return function(a,c){b(c,a)}}function Za(){for(var b=ia.length,a;b;){b--;a=ia[b].charCodeAt(0);if(57==a)return ia[b]="A",ia.join("");if(90==a)ia[b]="0";else return ia[b]=String.fromCharCode(a+1),ia.join("")}ia.unshift("0");return ia.join("")}function Pb(b,a){a?b.$$hashKey=a:delete b.$$hashKey}function u(b){var a=b.$$hashKey;q(arguments,function(a){a!==b&&q(a,function(a,c){b[c]=a})});Pb(b,a);return b}function U(b){return parseInt(b, -10)}function Qb(b,a){return u(new (u(function(){},{prototype:b})),a)}function t(){}function za(b){return b}function aa(b){return function(){return b}}function x(b){return"undefined"==typeof b}function z(b){return"undefined"!=typeof b}function T(b){return null!=b&&"object"==typeof b}function D(b){return"string"==typeof b}function qb(b){return"number"==typeof b}function Ja(b){return"[object Date]"==Ka.apply(b)}function J(b){return"[object Array]"==Ka.apply(b)}function B(b){return"function"==typeof b} -function $a(b){return"[object RegExp]"==Ka.apply(b)}function ya(b){return b&&b.document&&b.location&&b.alert&&b.setInterval}function Kc(b){return b&&(b.nodeName||b.on&&b.find)}function Lc(b,a,c){var d=[];q(b,function(b,f,g){d.push(a.call(c,b,f,g))});return d}function ab(b,a){if(b.indexOf)return b.indexOf(a);for(var c=0;c<b.length;c++)if(a===b[c])return c;return-1}function La(b,a){var c=ab(b,a);0<=c&&b.splice(c,1);return a}function da(b,a){if(ya(b)||b&&b.$evalAsync&&b.$watch)throw Ma("cpws");if(a){if(b=== -a)throw Ma("cpi");if(J(b))for(var c=a.length=0;c<b.length;c++)a.push(da(b[c]));else{c=a.$$hashKey;q(a,function(b,c){delete a[c]});for(var d in b)a[d]=da(b[d]);Pb(a,c)}}else(a=b)&&(J(b)?a=da(b,[]):Ja(b)?a=new Date(b.getTime()):$a(b)?a=RegExp(b.source):T(b)&&(a=da(b,{})));return a}function Mc(b,a){a=a||{};for(var c in b)b.hasOwnProperty(c)&&"$$"!==c.substr(0,2)&&(a[c]=b[c]);return a}function Aa(b,a){if(b===a)return!0;if(null===b||null===a)return!1;if(b!==b&&a!==a)return!0;var c=typeof b,d;if(c==typeof a&& -"object"==c)if(J(b)){if(!J(a))return!1;if((c=b.length)==a.length){for(d=0;d<c;d++)if(!Aa(b[d],a[d]))return!1;return!0}}else{if(Ja(b))return Ja(a)&&b.getTime()==a.getTime();if($a(b)&&$a(a))return b.toString()==a.toString();if(b&&b.$evalAsync&&b.$watch||a&&a.$evalAsync&&a.$watch||ya(b)||ya(a)||J(a))return!1;c={};for(d in b)if("$"!==d.charAt(0)&&!B(b[d])){if(!Aa(b[d],a[d]))return!1;c[d]=!0}for(d in a)if(!c.hasOwnProperty(d)&&"$"!==d.charAt(0)&&a[d]!==s&&!B(a[d]))return!1;return!0}return!1}function Rb(){return O.securityPolicy&& -O.securityPolicy.isActive||O.querySelector&&!(!O.querySelector("[ng-csp]")&&!O.querySelector("[data-ng-csp]"))}function rb(b,a){var c=2<arguments.length?ta.call(arguments,2):[];return!B(a)||a instanceof RegExp?a:c.length?function(){return arguments.length?a.apply(b,c.concat(ta.call(arguments,0))):a.apply(b,c)}:function(){return arguments.length?a.apply(b,arguments):a.call(b)}}function Nc(b,a){var c=a;"string"===typeof b&&"$"===b.charAt(0)?c=s:ya(a)?c="$WINDOW":a&&O===a?c="$DOCUMENT":a&&(a.$evalAsync&& -a.$watch)&&(c="$SCOPE");return c}function ma(b,a){return"undefined"===typeof b?s:JSON.stringify(b,Nc,a?" ":null)}function Sb(b){return D(b)?JSON.parse(b):b}function Na(b){b&&0!==b.length?(b=w(""+b),b=!("f"==b||"0"==b||"false"==b||"no"==b||"n"==b||"[]"==b)):b=!1;return b}function ea(b){b=y(b).clone();try{b.html("")}catch(a){}var c=y("<div>").append(b).html();try{return 3===b[0].nodeType?w(c):c.match(/^(<[^>]+>)/)[1].replace(/^<([\w\-]+)/,function(a,b){return"<"+w(b)})}catch(d){return w(c)}}function Tb(b){try{return decodeURIComponent(b)}catch(a){}} -function Ub(b){var a={},c,d;q((b||"").split("&"),function(b){b&&(c=b.split("="),d=Tb(c[0]),z(d)&&(b=z(c[1])?Tb(c[1]):!0,a[d]?J(a[d])?a[d].push(b):a[d]=[a[d],b]:a[d]=b))});return a}function Vb(b){var a=[];q(b,function(b,d){J(b)?q(b,function(b){a.push(ua(d,!0)+(!0===b?"":"="+ua(b,!0)))}):a.push(ua(d,!0)+(!0===b?"":"="+ua(b,!0)))});return a.length?a.join("&"):""}function sb(b){return ua(b,!0).replace(/%26/gi,"&").replace(/%3D/gi,"=").replace(/%2B/gi,"+")}function ua(b,a){return encodeURIComponent(b).replace(/%40/gi, -"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%20/g,a?"%20":"+")}function Oc(b,a){function c(a){a&&d.push(a)}var d=[b],e,f,g=["ng:app","ng-app","x-ng-app","data-ng-app"],h=/\sng[:\-]app(:\s*([\w\d_]+);?)?\s/;q(g,function(a){g[a]=!0;c(O.getElementById(a));a=a.replace(":","\\:");b.querySelectorAll&&(q(b.querySelectorAll("."+a),c),q(b.querySelectorAll("."+a+"\\:"),c),q(b.querySelectorAll("["+a+"]"),c))});q(d,function(a){if(!e){var b=h.exec(" "+a.className+" ");b?(e=a,f= -(b[2]||"").replace(/\s+/g,",")):q(a.attributes,function(b){!e&&g[b.name]&&(e=a,f=b.value)})}});e&&a(e,f?[f]:[])}function Wb(b,a){var c=function(){b=y(b);if(b.injector()){var c=b[0]===O?"document":ea(b);throw Ma("btstrpd",c);}a=a||[];a.unshift(["$provide",function(a){a.value("$rootElement",b)}]);a.unshift("ng");c=Xb(a);c.invoke(["$rootScope","$rootElement","$compile","$injector","$animate",function(a,b,c,d,e){a.$apply(function(){b.data("$injector",d);c(b)(a)})}]);return c},d=/^NG_DEFER_BOOTSTRAP!/; -if(W&&!d.test(W.name))return c();W.name=W.name.replace(d,"");bb.resumeBootstrap=function(b){q(b,function(b){a.push(b)});c()}}function cb(b,a){a=a||"_";return b.replace(Pc,function(b,d){return(d?a:"")+b.toLowerCase()})}function tb(b,a,c){if(!b)throw Ma("areq",a||"?",c||"required");return b}function Oa(b,a,c){c&&J(b)&&(b=b[b.length-1]);tb(B(b),a,"not a function, got "+(b&&"object"==typeof b?b.constructor.name||"Object":typeof b));return b}function na(b,a){if("hasOwnProperty"===b)throw Ma("badname", -a);}function ub(b,a,c){if(!a)return b;a=a.split(".");for(var d,e=b,f=a.length,g=0;g<f;g++)d=a[g],b&&(b=(e=b)[d]);return!c&&B(b)?rb(e,b):b}function vb(b){if(b.startNode===b.endNode)return y(b.startNode);var a=b.startNode,c=[a];do{a=a.nextSibling;if(!a)break;c.push(a)}while(a!==b.endNode);return y(c)}function Qc(b){function a(a,b,c){return a[b]||(a[b]=c())}var c=L("$injector");return a(a(b,"angular",Object),"module",function(){var b={};return function(e,f,g){na(e,"module");f&&b.hasOwnProperty(e)&&(b[e]= -null);return a(b,e,function(){function a(c,d,e){return function(){b[e||"push"]([c,d,arguments]);return n}}if(!f)throw c("nomod",e);var b=[],d=[],l=a("$injector","invoke"),n={_invokeQueue:b,_runBlocks:d,requires:f,name:e,provider:a("$provide","provider"),factory:a("$provide","factory"),service:a("$provide","service"),value:a("$provide","value"),constant:a("$provide","constant","unshift"),animation:a("$animateProvider","register"),filter:a("$filterProvider","register"),controller:a("$controllerProvider", -"register"),directive:a("$compileProvider","directive"),config:l,run:function(a){d.push(a);return this}};g&&l(g);return n})}})}function Pa(b){return b.replace(Rc,function(a,b,d,e){return e?d.toUpperCase():d}).replace(Sc,"Moz$1")}function wb(b,a,c,d){function e(b){var e=c&&b?[this.filter(b)]:[this],m=a,k,l,n,r,p,C;if(!d||null!=b)for(;e.length;)for(k=e.shift(),l=0,n=k.length;l<n;l++)for(r=y(k[l]),m?r.triggerHandler("$destroy"):m=!m,p=0,r=(C=r.children()).length;p<r;p++)e.push(Ba(C[p]));return f.apply(this, -arguments)}var f=Ba.fn[b],f=f.$original||f;e.$original=f;Ba.fn[b]=e}function Q(b){if(b instanceof Q)return b;if(!(this instanceof Q)){if(D(b)&&"<"!=b.charAt(0))throw xb("nosel");return new Q(b)}if(D(b)){var a=O.createElement("div");a.innerHTML="<div> </div>"+b;a.removeChild(a.firstChild);yb(this,a.childNodes);y(O.createDocumentFragment()).append(this)}else yb(this,b)}function zb(b){return b.cloneNode(!0)}function Qa(b){Yb(b);var a=0;for(b=b.childNodes||[];a<b.length;a++)Qa(b[a])}function Zb(b, -a,c,d){if(z(d))throw xb("offargs");var e=ja(b,"events");ja(b,"handle")&&(x(a)?q(e,function(a,c){Ab(b,c,a);delete e[c]}):q(a.split(" "),function(a){x(c)?(Ab(b,a,e[a]),delete e[a]):La(e[a]||[],c)}))}function Yb(b,a){var c=b[db],d=Ra[c];d&&(a?delete Ra[c].data[a]:(d.handle&&(d.events.$destroy&&d.handle({},"$destroy"),Zb(b)),delete Ra[c],b[db]=s))}function ja(b,a,c){var d=b[db],d=Ra[d||-1];if(z(c))d||(b[db]=d=++Tc,d=Ra[d]={}),d[a]=c;else return d&&d[a]}function $b(b,a,c){var d=ja(b,"data"),e=z(c),f=!e&& -z(a),g=f&&!T(a);d||g||ja(b,"data",d={});if(e)d[a]=c;else if(f){if(g)return d&&d[a];u(d,a)}else return d}function Bb(b,a){return b.getAttribute?-1<(" "+(b.getAttribute("class")||"")+" ").replace(/[\n\t]/g," ").indexOf(" "+a+" "):!1}function Cb(b,a){a&&b.setAttribute&&q(a.split(" "),function(a){b.setAttribute("class",Y((" "+(b.getAttribute("class")||"")+" ").replace(/[\n\t]/g," ").replace(" "+Y(a)+" "," ")))})}function Db(b,a){if(a&&b.setAttribute){var c=(" "+(b.getAttribute("class")||"")+" ").replace(/[\n\t]/g, -" ");q(a.split(" "),function(a){a=Y(a);-1===c.indexOf(" "+a+" ")&&(c+=a+" ")});b.setAttribute("class",Y(c))}}function yb(b,a){if(a){a=a.nodeName||!z(a.length)||ya(a)?[a]:a;for(var c=0;c<a.length;c++)b.push(a[c])}}function ac(b,a){return eb(b,"$"+(a||"ngController")+"Controller")}function eb(b,a,c){b=y(b);9==b[0].nodeType&&(b=b.find("html"));for(a=J(a)?a:[a];b.length;){for(var d=0,e=a.length;d<e;d++)if((c=b.data(a[d]))!==s)return c;b=b.parent()}}function bc(b,a){var c=fb[a.toLowerCase()];return c&& -cc[b.nodeName]&&c}function Uc(b,a){var c=function(c,e){c.preventDefault||(c.preventDefault=function(){c.returnValue=!1});c.stopPropagation||(c.stopPropagation=function(){c.cancelBubble=!0});c.target||(c.target=c.srcElement||O);if(x(c.defaultPrevented)){var f=c.preventDefault;c.preventDefault=function(){c.defaultPrevented=!0;f.call(c)};c.defaultPrevented=!1}c.isDefaultPrevented=function(){return c.defaultPrevented||!1===c.returnValue};q(a[e||c.type],function(a){a.call(b,c)});8>=P?(c.preventDefault= -null,c.stopPropagation=null,c.isDefaultPrevented=null):(delete c.preventDefault,delete c.stopPropagation,delete c.isDefaultPrevented)};c.elem=b;return c}function Ca(b){var a=typeof b,c;"object"==a&&null!==b?"function"==typeof(c=b.$$hashKey)?c=b.$$hashKey():c===s&&(c=b.$$hashKey=Za()):c=b;return a+":"+c}function Sa(b){q(b,this.put,this)}function dc(b){var a,c;"function"==typeof b?(a=b.$inject)||(a=[],b.length&&(c=b.toString().replace(Vc,""),c=c.match(Wc),q(c[1].split(Xc),function(b){b.replace(Yc,function(b, -c,d){a.push(d)})})),b.$inject=a):J(b)?(c=b.length-1,Oa(b[c],"fn"),a=b.slice(0,c)):Oa(b,"fn",!0);return a}function Xb(b){function a(a){return function(b,c){if(T(b))q(b,Ob(a));else return a(b,c)}}function c(a,b){na(a,"service");if(B(b)||J(b))b=n.instantiate(b);if(!b.$get)throw Ta("pget",a);return l[a+h]=b}function d(a,b){return c(a,{$get:b})}function e(a){var b=[],c,d,f,h;q(a,function(a){if(!k.get(a)){k.put(a,!0);try{if(D(a))for(c=Ua(a),b=b.concat(e(c.requires)).concat(c._runBlocks),d=c._invokeQueue, -f=0,h=d.length;f<h;f++){var g=d[f],m=n.get(g[0]);m[g[1]].apply(m,g[2])}else B(a)?b.push(n.invoke(a)):J(a)?b.push(n.invoke(a)):Oa(a,"module")}catch(l){throw J(a)&&(a=a[a.length-1]),l.message&&(l.stack&&-1==l.stack.indexOf(l.message))&&(l=l.message+"\n"+l.stack),Ta("modulerr",a,l.stack||l.message||l);}}});return b}function f(a,b){function c(d){if(a.hasOwnProperty(d)){if(a[d]===g)throw Ta("cdep",m.join(" <- "));return a[d]}try{return m.unshift(d),a[d]=g,a[d]=b(d)}finally{m.shift()}}function d(a,b,e){var f= -[],h=dc(a),g,k,m;k=0;for(g=h.length;k<g;k++){m=h[k];if("string"!==typeof m)throw Ta("itkn",m);f.push(e&&e.hasOwnProperty(m)?e[m]:c(m))}a.$inject||(a=a[g]);switch(b?-1:f.length){case 0:return a();case 1:return a(f[0]);case 2:return a(f[0],f[1]);case 3:return a(f[0],f[1],f[2]);case 4:return a(f[0],f[1],f[2],f[3]);case 5:return a(f[0],f[1],f[2],f[3],f[4]);case 6:return a(f[0],f[1],f[2],f[3],f[4],f[5]);case 7:return a(f[0],f[1],f[2],f[3],f[4],f[5],f[6]);case 8:return a(f[0],f[1],f[2],f[3],f[4],f[5],f[6], -f[7]);case 9:return a(f[0],f[1],f[2],f[3],f[4],f[5],f[6],f[7],f[8]);case 10:return a(f[0],f[1],f[2],f[3],f[4],f[5],f[6],f[7],f[8],f[9]);default:return a.apply(b,f)}}return{invoke:d,instantiate:function(a,b){var c=function(){},e;c.prototype=(J(a)?a[a.length-1]:a).prototype;c=new c;e=d(a,c,b);return T(e)||B(e)?e:c},get:c,annotate:dc,has:function(b){return l.hasOwnProperty(b+h)||a.hasOwnProperty(b)}}}var g={},h="Provider",m=[],k=new Sa,l={$provide:{provider:a(c),factory:a(d),service:a(function(a,b){return d(a, -["$injector",function(a){return a.instantiate(b)}])}),value:a(function(a,b){return d(a,aa(b))}),constant:a(function(a,b){na(a,"constant");l[a]=b;r[a]=b}),decorator:function(a,b){var c=n.get(a+h),d=c.$get;c.$get=function(){var a=p.invoke(d,c);return p.invoke(b,null,{$delegate:a})}}}},n=l.$injector=f(l,function(){throw Ta("unpr",m.join(" <- "));}),r={},p=r.$injector=f(r,function(a){a=n.get(a+h);return p.invoke(a.$get,a)});q(e(b),function(a){p.invoke(a||t)});return p}function Zc(){var b=!0;this.disableAutoScrolling= -function(){b=!1};this.$get=["$window","$location","$rootScope",function(a,c,d){function e(a){var b=null;q(a,function(a){b||"a"!==w(a.nodeName)||(b=a)});return b}function f(){var b=c.hash(),d;b?(d=g.getElementById(b))?d.scrollIntoView():(d=e(g.getElementsByName(b)))?d.scrollIntoView():"top"===b&&a.scrollTo(0,0):a.scrollTo(0,0)}var g=a.document;b&&d.$watch(function(){return c.hash()},function(){d.$evalAsync(f)});return f}]}function $c(b,a,c,d){function e(a){try{a.apply(null,ta.call(arguments,1))}finally{if(C--, -0===C)for(;H.length;)try{H.pop()()}catch(b){c.error(b)}}}function f(a,b){(function gb(){q(I,function(a){a()});A=b(gb,a)})()}function g(){v=null;S!=h.url()&&(S=h.url(),q(Z,function(a){a(h.url())}))}var h=this,m=a[0],k=b.location,l=b.history,n=b.setTimeout,r=b.clearTimeout,p={};h.isMock=!1;var C=0,H=[];h.$$completeOutstandingRequest=e;h.$$incOutstandingRequestCount=function(){C++};h.notifyWhenNoOutstandingRequests=function(a){q(I,function(a){a()});0===C?a():H.push(a)};var I=[],A;h.addPollFn=function(a){x(A)&& -f(100,n);I.push(a);return a};var S=k.href,G=a.find("base"),v=null;h.url=function(a,c){k!==b.location&&(k=b.location);if(a){if(S!=a)return S=a,d.history?c?l.replaceState(null,"",a):(l.pushState(null,"",a),G.attr("href",G.attr("href"))):(v=a,c?k.replace(a):k.href=a),h}else return v||k.href.replace(/%27/g,"'")};var Z=[],E=!1;h.onUrlChange=function(a){if(!E){if(d.history)y(b).on("popstate",g);if(d.hashchange)y(b).on("hashchange",g);else h.addPollFn(g);E=!0}Z.push(a);return a};h.baseHref=function(){var a= -G.attr("href");return a?a.replace(/^https?\:\/\/[^\/]*/,""):""};var oa={},$="",va=h.baseHref();h.cookies=function(a,b){var d,e,f,h;if(a)b===s?m.cookie=escape(a)+"=;path="+va+";expires=Thu, 01 Jan 1970 00:00:00 GMT":D(b)&&(d=(m.cookie=escape(a)+"="+escape(b)+";path="+va).length+1,4096<d&&c.warn("Cookie '"+a+"' possibly not set or overflowed because it was too large ("+d+" > 4096 bytes)!"));else{if(m.cookie!==$)for($=m.cookie,d=$.split("; "),oa={},f=0;f<d.length;f++)e=d[f],h=e.indexOf("="),0<h&&(a= -unescape(e.substring(0,h)),oa[a]===s&&(oa[a]=unescape(e.substring(h+1))));return oa}};h.defer=function(a,b){var c;C++;c=n(function(){delete p[c];e(a)},b||0);p[c]=!0;return c};h.defer.cancel=function(a){return p[a]?(delete p[a],r(a),e(t),!0):!1}}function bd(){this.$get=["$window","$log","$sniffer","$document",function(b,a,c,d){return new $c(b,d,a,c)}]}function cd(){this.$get=function(){function b(b,d){function e(a){a!=n&&(r?r==a&&(r=a.n):r=a,f(a.n,a.p),f(a,n),n=a,n.n=null)}function f(a,b){a!=b&&(a&& -(a.p=b),b&&(b.n=a))}if(b in a)throw L("$cacheFactory")("iid",b);var g=0,h=u({},d,{id:b}),m={},k=d&&d.capacity||Number.MAX_VALUE,l={},n=null,r=null;return a[b]={put:function(a,b){var c=l[a]||(l[a]={key:a});e(c);if(!x(b))return a in m||g++,m[a]=b,g>k&&this.remove(r.key),b},get:function(a){var b=l[a];if(b)return e(b),m[a]},remove:function(a){var b=l[a];b&&(b==n&&(n=b.p),b==r&&(r=b.n),f(b.n,b.p),delete l[a],delete m[a],g--)},removeAll:function(){m={};g=0;l={};n=r=null},destroy:function(){l=h=m=null;delete a[b]}, -info:function(){return u({},h,{size:g})}}}var a={};b.info=function(){var b={};q(a,function(a,e){b[e]=a.info()});return b};b.get=function(b){return a[b]};return b}}function dd(){this.$get=["$cacheFactory",function(b){return b("templates")}]}function ec(b){var a={},c="Directive",d=/^\s*directive\:\s*([\d\w\-_]+)\s+(.*)$/,e=/(([\d\w\-_]+)(?:\:([^;]+))?;?)/,f=/^\s*(https?|ftp|mailto|tel|file):/,g=/^\s*(https?|ftp|file):|data:image\//,h=/^(on[a-z]+|formaction)$/;this.directive=function k(d,e){na(d,"directive"); -D(d)?(tb(e,"directiveFactory"),a.hasOwnProperty(d)||(a[d]=[],b.factory(d+c,["$injector","$exceptionHandler",function(b,c){var e=[];q(a[d],function(a,f){try{var h=b.invoke(a);B(h)?h={compile:aa(h)}:!h.compile&&h.link&&(h.compile=aa(h.link));h.priority=h.priority||0;h.index=f;h.name=h.name||d;h.require=h.require||h.controller&&h.name;h.restrict=h.restrict||"A";e.push(h)}catch(g){c(g)}});return e}])),a[d].push(e)):q(d,Ob(k));return this};this.aHrefSanitizationWhitelist=function(a){return z(a)?(f=a,this): -f};this.imgSrcSanitizationWhitelist=function(a){return z(a)?(g=a,this):g};this.$get=["$injector","$interpolate","$exceptionHandler","$http","$templateCache","$parse","$controller","$rootScope","$document","$sce","$animate",function(b,l,n,r,p,C,H,I,A,S,G){function v(a,b,c,d,e){a instanceof y||(a=y(a));q(a,function(b,c){3==b.nodeType&&b.nodeValue.match(/\S+/)&&(a[c]=y(b).wrap("<span></span>").parent()[0])});var f=E(a,b,a,c,d,e);return function(b,c){tb(b,"scope");for(var d=c?Da.clone.call(a):a,e=0,h= -d.length;e<h;e++){var g=d[e];1!=g.nodeType&&9!=g.nodeType||d.eq(e).data("$scope",b)}Z(d,"ng-scope");c&&c(d,b);f&&f(b,d,d);return d}}function Z(a,b){try{a.addClass(b)}catch(c){}}function E(a,b,c,d,e,f){function h(a,c,d,e){var f,k,l,n,p,r,C,X=[];p=0;for(r=c.length;p<r;p++)X.push(c[p]);C=p=0;for(r=g.length;p<r;C++)k=X[C],c=g[p++],f=g[p++],l=y(k),c?(c.scope?(n=a.$new(),l.data("$scope",n),Z(l,"ng-scope")):n=a,(l=c.transclude)||!e&&b?c(f,n,k,d,function(b){return function(c){var d=a.$new();d.$$transcluded= -!0;return b(d,c).on("$destroy",rb(d,d.$destroy))}}(l||b)):c(f,n,k,s,e)):f&&f(a,k.childNodes,s,e)}for(var g=[],k,l,n,p=0;p<a.length;p++)l=new R,k=oa(a[p],[],l,0===p?d:s,e),k=(f=k.length?M(k,a[p],l,b,c,null,[],[],f):null)&&f.terminal||!a[p].childNodes||!a[p].childNodes.length?null:E(a[p].childNodes,f?f.transclude:b),g.push(f),g.push(k),n=n||f||k,f=null;return n?h:null}function oa(a,b,c,f,h){var g=c.$attr,k;switch(a.nodeType){case 1:N(b,ka(Ea(a).toLowerCase()),"E",f,h);var l,n,p;k=a.attributes;for(var r= -0,C=k&&k.length;r<C;r++){var H=!1,v=!1;l=k[r];if(!P||8<=P||l.specified){n=l.name;p=ka(n);Fa.test(p)&&(n=cb(p.substr(6),"-"));var q=p.replace(/(Start|End)$/,"");p===q+"Start"&&(H=n,v=n.substr(0,n.length-5)+"end",n=n.substr(0,n.length-6));p=ka(n.toLowerCase());g[p]=n;c[p]=l=Y(P&&"href"==n?decodeURIComponent(a.getAttribute(n,2)):l.value);bc(a,p)&&(c[p]=!0);w(a,b,l,p);N(b,p,"A",f,h,H,v)}}a=a.className;if(D(a)&&""!==a)for(;k=e.exec(a);)p=ka(k[2]),N(b,p,"C",f,h)&&(c[p]=Y(k[3])),a=a.substr(k.index+k[0].length); -break;case 3:t(b,a.nodeValue);break;case 8:try{if(k=d.exec(a.nodeValue))p=ka(k[1]),N(b,p,"M",f,h)&&(c[p]=Y(k[2]))}catch(S){}}b.sort(wa);return b}function $(a,b,c){var d=[],e=0;if(b&&a.hasAttribute&&a.hasAttribute(b)){do{if(!a)throw fa("uterdir",b,c);1==a.nodeType&&(a.hasAttribute(b)&&e++,a.hasAttribute(c)&&e--);d.push(a);a=a.nextSibling}while(0<e)}else d.push(a);return y(d)}function va(a,b,c){return function(d,e,f,h){e=$(e[0],b,c);return a(d,e,f,h)}}function M(a,b,c,d,e,f,h,k,g){function p(a,b,c, -d){if(a){c&&(a=va(a,c,d));a.require=F.require;if(G===F||F.$$isolateScope)a=Q(a,{isolateScope:!0});h.push(a)}if(b){c&&(b=va(b,c,d));b.require=F.require;if(G===F||F.$$isolateScope)b=Q(b,{isolateScope:!0});k.push(b)}}function r(a,b){var c,d="data",e=!1;if(D(a)){for(;"^"==(c=a.charAt(0))||"?"==c;)a=a.substr(1),"^"==c&&(d="inheritedData"),e=e||"?"==c;c=b[d]("$"+a+"Controller");8==b[0].nodeType&&b[0].$$controller&&(c=c||b[0].$$controller,b[0].$$controller=null);if(!c&&!e)throw fa("ctreq",a,ba);}else J(a)&& -(c=[],q(a,function(a){c.push(r(a,b))}));return c}function S(a,d,e,f,g){var p,X,v,E,I,K;p=b===e?c:Mc(c,new R(y(e),c.$attr));X=p.$$element;if(G){var va=/^\s*([@=&])(\??)\s*(\w*)\s*$/;f=y(e);K=d.$new(!0);N&&N===G.$$originalDirective?f.data("$isolateScope",K):f.data("$isolateScopeNoTemplate",K);Z(f,"ng-isolate-scope");q(G.scope,function(a,b){var c=a.match(va)||[],e=c[3]||b,f="?"==c[2],c=c[1],h,g,k;K.$$isolateBindings[b]=c+e;switch(c){case "@":p.$observe(e,function(a){K[b]=a});p.$$observers[e].$$scope= -d;p[e]&&(K[b]=l(p[e])(d));break;case "=":if(f&&!p[e])break;g=C(p[e]);k=g.assign||function(){h=K[b]=g(d);throw fa("nonassign",p[e],G.name);};h=K[b]=g(d);K.$watch(function(){var a=g(d);a!==K[b]&&(a!==h?h=K[b]=a:k(d,a=h=K[b]));return a});break;case "&":g=C(p[e]);K[b]=function(a){return g(d,a)};break;default:throw fa("iscp",G.name,b,a);}})}A&&q(A,function(a){var b={$scope:a===G||a.$$isolateScope?K:d,$element:X,$attrs:p,$transclude:g},c;I=a.controller;"@"==I&&(I=p[a.name]);c=H(I,b);8==X[0].nodeType?X[0].$$controller= -c:X.data("$"+a.name+"Controller",c);a.controllerAs&&(b.$scope[a.controllerAs]=c)});f=0;for(v=h.length;f<v;f++)try{E=h[f],E(E.isolateScope?K:d,X,p,E.require&&r(E.require,X))}catch(M){n(M,ea(X))}f=d;G&&(G.template||null===G.templateUrl)&&(f=K);a&&a(f,e.childNodes,s,g);for(f=k.length-1;0<=f;f--)try{E=k[f],E(E.isolateScope?K:d,X,p,E.require&&r(E.require,X))}catch($){n($,ea(X))}}g=g||{};var E=-Number.MAX_VALUE,I,A=g.controllerDirectives,G=g.newIsolateScopeDirective,N=g.templateDirective;g=g.transcludeDirective; -for(var M=c.$$element=y(b),F,ba,t,wa=d,x,ga=0,w=a.length;ga<w;ga++){F=a[ga];var u=F.$$start,Fa=F.$$end;u&&(M=$(b,u,Fa));t=s;if(E>F.priority)break;if(t=F.scope)I=I||F,F.templateUrl||(Va("new/isolated scope",G,F,M),T(t)&&(G=F));ba=F.name;!F.templateUrl&&F.controller&&(t=F.controller,A=A||{},Va("'"+ba+"' controller",A[ba],F,M),A[ba]=F);if(t=F.transclude)F.$$tlb||(Va("transclusion",g,F,M),g=F),"element"==t?(E=F.priority,t=$(b,u,Fa),M=c.$$element=y(O.createComment(" "+ba+": "+c[ba]+" ")),b=M[0],L(e,y(ta.call(t, -0)),b),wa=v(t,d,E,f&&f.name,{transcludeDirective:g})):(t=y(zb(b)).contents(),M.html(""),wa=v(t,d));if(F.template)if(Va("template",N,F,M),N=F,t=B(F.template)?F.template(M,c):F.template,t=fc(t),F.replace){f=F;t=y("<div>"+Y(t)+"</div>").contents();b=t[0];if(1!=t.length||1!==b.nodeType)throw fa("tplrt",ba,"");L(e,M,b);w={$attr:{}};t=oa(b,[],w);var P=a.splice(ga+1,a.length-(ga+1));G&&z(t);a=a.concat(t).concat(P);gb(c,w);w=a.length}else M.html(t);if(F.templateUrl)Va("template",N,F,M),N=F,F.replace&&(f= -F),S=ad(a.splice(ga,a.length-ga),M,c,e,wa,h,k,{controllerDirectives:A,newIsolateScopeDirective:G,templateDirective:N,transcludeDirective:g}),w=a.length;else if(F.compile)try{x=F.compile(M,c,wa),B(x)?p(null,x,u,Fa):x&&p(x.pre,x.post,u,Fa)}catch(ed){n(ed,ea(M))}F.terminal&&(S.terminal=!0,E=Math.max(E,F.priority))}S.scope=I&&!0===I.scope;S.transclude=g&&wa;return S}function z(a){for(var b=0,c=a.length;b<c;b++)a[b]=Qb(a[b],{$$isolateScope:!0})}function N(d,e,f,h,g,l,p){if(e===g)return null;g=null;if(a.hasOwnProperty(e)){var r; -e=b.get(e+c);for(var C=0,H=e.length;C<H;C++)try{r=e[C],(h===s||h>r.priority)&&-1!=r.restrict.indexOf(f)&&(l&&(r=Qb(r,{$$start:l,$$end:p})),d.push(r),g=r)}catch(E){n(E)}}return g}function gb(a,b){var c=b.$attr,d=a.$attr,e=a.$$element;q(a,function(d,e){"$"!=e.charAt(0)&&(b[e]&&(d+=("style"===e?";":" ")+b[e]),a.$set(e,d,!0,c[e]))});q(b,function(b,f){"class"==f?(Z(e,b),a["class"]=(a["class"]?a["class"]+" ":"")+b):"style"==f?e.attr("style",e.attr("style")+";"+b):"$"==f.charAt(0)||a.hasOwnProperty(f)|| -(a[f]=b,d[f]=c[f])})}function ad(a,b,c,d,e,f,h,g){var k=[],n,l,C=b[0],H=a.shift(),v=u({},H,{templateUrl:null,transclude:null,replace:null,$$originalDirective:H}),I=B(H.templateUrl)?H.templateUrl(b,c):H.templateUrl;b.html("");r.get(S.getTrustedResourceUrl(I),{cache:p}).success(function(p){var r;p=fc(p);if(H.replace){p=y("<div>"+Y(p)+"</div>").contents();r=p[0];if(1!=p.length||1!==r.nodeType)throw fa("tplrt",H.name,I);p={$attr:{}};L(d,b,r);var S=oa(r,[],p);T(H.scope)&&z(S);a=S.concat(a);gb(c,p)}else r= -C,b.html(p);a.unshift(v);n=M(a,r,c,e,b,H,f,h,g);q(d,function(a,c){a==r&&(d[c]=b[0])});for(l=E(b[0].childNodes,e);k.length;){p=k.shift();var S=k.shift(),Z=k.shift(),G=k.shift(),A=b[0];S!==C&&(A=zb(r),L(Z,y(S),A));n(l,p,A,d,G)}k=null}).error(function(a,b,c,d){throw fa("tpload",d.url);});return function(a,b,c,d,e){k?(k.push(b),k.push(c),k.push(d),k.push(e)):n(l,b,c,d,e)}}function wa(a,b){var c=b.priority-a.priority;return 0!==c?c:a.name!==b.name?a.name<b.name?-1:1:a.index-b.index}function Va(a,b,c,d){if(b)throw fa("multidir", -b.name,c.name,a,ea(d));}function t(a,b){var c=l(b,!0);c&&a.push({priority:0,compile:aa(function(a,b){var d=b.parent(),e=d.data("$binding")||[];e.push(c);Z(d.data("$binding",e),"ng-binding");a.$watch(c,function(a){b[0].nodeValue=a})})})}function x(a,b){if("xlinkHref"==b||"IMG"!=Ea(a)&&("src"==b||"ngSrc"==b))return S.RESOURCE_URL}function w(a,b,c,d){var e=l(c,!0);if(e){if("multiple"===d&&"SELECT"===Ea(a))throw fa("selmulti",ea(a));b.push({priority:100,compile:function(){return{pre:function(b,c,f){c= -f.$$observers||(f.$$observers={});if(h.test(d))throw fa("nodomevents");if(e=l(f[d],!0,x(a,d)))f[d]=e(b),(c[d]||(c[d]=[])).$$inter=!0,(f.$$observers&&f.$$observers[d].$$scope||b).$watch(e,function(a){f.$set(d,a)})}}}})}}function L(a,b,c){var d=b[0],e=b.length,f=d.parentNode,h,g;if(a)for(h=0,g=a.length;h<g;h++)if(a[h]==d){a[h++]=c;g=h+e-1;for(var k=a.length;h<k;h++,g++)g<k?a[h]=a[g]:delete a[h];a.length-=e-1;break}f&&f.replaceChild(c,d);a=O.createDocumentFragment();a.appendChild(d);c[y.expando]=d[y.expando]; -d=1;for(e=b.length;d<e;d++)f=b[d],y(f).remove(),a.appendChild(f),delete b[d];b[0]=c;b.length=1}function Q(a,b){return u(function(){return a.apply(null,arguments)},a,b)}var R=function(a,b){this.$$element=a;this.$attr=b||{}};R.prototype={$normalize:ka,$addClass:function(a){a&&0<a.length&&G.addClass(this.$$element,a)},$removeClass:function(a){a&&0<a.length&&G.removeClass(this.$$element,a)},$set:function(a,b,c,d){function e(a,b){var c=[],d=a.split(/\s+/),f=b.split(/\s+/),h=0;a:for(;h<d.length;h++){for(var g= -d[h],k=0;k<f.length;k++)if(g==f[k])continue a;c.push(g)}return c}if("class"==a)b=b||"",c=this.$$element.attr("class")||"",this.$removeClass(e(c,b).join(" ")),this.$addClass(e(b,c).join(" "));else{var h=bc(this.$$element[0],a);h&&(this.$$element.prop(a,b),d=h);this[a]=b;d?this.$attr[a]=d:(d=this.$attr[a])||(this.$attr[a]=d=cb(a,"-"));h=Ea(this.$$element);if("A"===h&&"href"===a||"IMG"===h&&"src"===a)if(!P||8<=P)h=xa(b).href,""!==h&&("href"===a&&!h.match(f)||"src"===a&&!h.match(g))&&(this[a]=b="unsafe:"+ -h);!1!==c&&(null===b||b===s?this.$$element.removeAttr(d):this.$$element.attr(d,b))}(c=this.$$observers)&&q(c[a],function(a){try{a(b)}catch(c){n(c)}})},$observe:function(a,b){var c=this,d=c.$$observers||(c.$$observers={}),e=d[a]||(d[a]=[]);e.push(b);I.$evalAsync(function(){e.$$inter||b(c[a])});return b}};var ba=l.startSymbol(),ga=l.endSymbol(),fc="{{"==ba||"}}"==ga?za:function(a){return a.replace(/\{\{/g,ba).replace(/}}/g,ga)},Fa=/^ngAttr[A-Z]/;return v}]}function ka(b){return Pa(b.replace(fd,""))} -function gd(){var b={},a=/^(\S+)(\s+as\s+(\w+))?$/;this.register=function(a,d){na(a,"controller");T(a)?u(b,a):b[a]=d};this.$get=["$injector","$window",function(c,d){return function(e,f){var g,h,m;D(e)&&(g=e.match(a),h=g[1],m=g[3],e=b.hasOwnProperty(h)?b[h]:ub(f.$scope,h,!0)||ub(d,h,!0),Oa(e,h,!0));g=c.instantiate(e,f);if(m){if(!f||"object"!=typeof f.$scope)throw L("$controller")("noscp",h||e.name,m);f.$scope[m]=g}return g}}]}function hd(){this.$get=["$window",function(b){return y(b.document)}]}function id(){this.$get= -["$log",function(b){return function(a,c){b.error.apply(b,arguments)}}]}function gc(b){var a={},c,d,e;if(!b)return a;q(b.split("\n"),function(b){e=b.indexOf(":");c=w(Y(b.substr(0,e)));d=Y(b.substr(e+1));c&&(a[c]=a[c]?a[c]+(", "+d):d)});return a}function hc(b){var a=T(b)?b:s;return function(c){a||(a=gc(b));return c?a[w(c)]||null:a}}function ic(b,a,c){if(B(c))return c(b,a);q(c,function(c){b=c(b,a)});return b}function jd(){var b=/^\s*(\[|\{[^\{])/,a=/[\}\]]\s*$/,c=/^\)\]\}',?\n/,d={"Content-Type":"application/json;charset=utf-8"}, -e=this.defaults={transformResponse:[function(d){D(d)&&(d=d.replace(c,""),b.test(d)&&a.test(d)&&(d=Sb(d)));return d}],transformRequest:[function(a){return T(a)&&"[object File]"!==Ka.apply(a)?ma(a):a}],headers:{common:{Accept:"application/json, text/plain, */*"},post:d,put:d,patch:d},xsrfCookieName:"XSRF-TOKEN",xsrfHeaderName:"X-XSRF-TOKEN"},f=this.interceptors=[],g=this.responseInterceptors=[];this.$get=["$httpBackend","$browser","$cacheFactory","$rootScope","$q","$injector",function(a,b,c,d,n,r){function p(a){function c(a){var b= -u({},a,{data:ic(a.data,a.headers,d.transformResponse)});return 200<=a.status&&300>a.status?b:n.reject(b)}var d={transformRequest:e.transformRequest,transformResponse:e.transformResponse},f=function(a){function b(a){var c;q(a,function(b,d){B(b)&&(c=b(),null!=c?a[d]=c:delete a[d])})}var c=e.headers,d=u({},a.headers),f,h,c=u({},c.common,c[w(a.method)]);b(c);b(d);a:for(f in c){a=w(f);for(h in d)if(w(h)===a)continue a;d[f]=c[f]}return d}(a);u(d,a);d.headers=f;d.method=Ga(d.method);(a=Eb(d.url)?b.cookies()[d.xsrfCookieName|| -e.xsrfCookieName]:s)&&(f[d.xsrfHeaderName||e.xsrfHeaderName]=a);var h=[function(a){f=a.headers;var b=ic(a.data,hc(f),a.transformRequest);x(a.data)&&q(f,function(a,b){"content-type"===w(b)&&delete f[b]});x(a.withCredentials)&&!x(e.withCredentials)&&(a.withCredentials=e.withCredentials);return C(a,b,f).then(c,c)},s],g=n.when(d);for(q(A,function(a){(a.request||a.requestError)&&h.unshift(a.request,a.requestError);(a.response||a.responseError)&&h.push(a.response,a.responseError)});h.length;){a=h.shift(); -var k=h.shift(),g=g.then(a,k)}g.success=function(a){g.then(function(b){a(b.data,b.status,b.headers,d)});return g};g.error=function(a){g.then(null,function(b){a(b.data,b.status,b.headers,d)});return g};return g}function C(b,c,f){function g(a,b,c){q&&(200<=a&&300>a?q.put(s,[a,b,gc(c)]):q.remove(s));k(b,a,c);d.$$phase||d.$apply()}function k(a,c,d){c=Math.max(c,0);(200<=c&&300>c?r.resolve:r.reject)({data:a,status:c,headers:hc(d),config:b})}function m(){var a=ab(p.pendingRequests,b);-1!==a&&p.pendingRequests.splice(a, -1)}var r=n.defer(),C=r.promise,q,A,s=H(b.url,b.params);p.pendingRequests.push(b);C.then(m,m);(b.cache||e.cache)&&(!1!==b.cache&&"GET"==b.method)&&(q=T(b.cache)?b.cache:T(e.cache)?e.cache:I);if(q)if(A=q.get(s),z(A)){if(A.then)return A.then(m,m),A;J(A)?k(A[1],A[0],da(A[2])):k(A,200,{})}else q.put(s,C);x(A)&&a(b.method,s,c,g,f,b.timeout,b.withCredentials,b.responseType);return C}function H(a,b){if(!b)return a;var c=[];Jc(b,function(a,b){null===a||x(a)||(J(a)||(a=[a]),q(a,function(a){T(a)&&(a=ma(a)); -c.push(ua(b)+"="+ua(a))}))});return a+(-1==a.indexOf("?")?"?":"&")+c.join("&")}var I=c("$http"),A=[];q(f,function(a){A.unshift(D(a)?r.get(a):r.invoke(a))});q(g,function(a,b){var c=D(a)?r.get(a):r.invoke(a);A.splice(b,0,{response:function(a){return c(n.when(a))},responseError:function(a){return c(n.reject(a))}})});p.pendingRequests=[];(function(a){q(arguments,function(a){p[a]=function(b,c){return p(u(c||{},{method:a,url:b}))}})})("get","delete","head","jsonp");(function(a){q(arguments,function(a){p[a]= -function(b,c,d){return p(u(d||{},{method:a,url:b,data:c}))}})})("post","put");p.defaults=e;return p}]}function kd(){this.$get=["$browser","$window","$document",function(b,a,c){return ld(b,md,b.defer,a.angular.callbacks,c[0],a.location.protocol.replace(":",""))}]}function ld(b,a,c,d,e,f){function g(a,b){var c=e.createElement("script"),d=function(){e.body.removeChild(c);b&&b()};c.type="text/javascript";c.src=a;P?c.onreadystatechange=function(){/loaded|complete/.test(c.readyState)&&d()}:c.onload=c.onerror= -d;e.body.appendChild(c);return d}return function(e,m,k,l,n,r,p,C){function H(){A=-1;G&&G();v&&v.abort()}function I(a,d,e,h){var g=f||xa(m).protocol;Z&&c.cancel(Z);G=v=null;d="file"==g?e?200:404:d;a(1223==d?204:d,e,h);b.$$completeOutstandingRequest(t)}var A;b.$$incOutstandingRequestCount();m=m||b.url();if("jsonp"==w(e)){var s="_"+(d.counter++).toString(36);d[s]=function(a){d[s].data=a};var G=g(m.replace("JSON_CALLBACK","angular.callbacks."+s),function(){d[s].data?I(l,200,d[s].data):I(l,A||-2);delete d[s]})}else{var v= -new a;v.open(e,m,!0);q(n,function(a,b){z(a)&&v.setRequestHeader(b,a)});v.onreadystatechange=function(){if(4==v.readyState){var a=v.getAllResponseHeaders();I(l,A||v.status,v.responseType?v.response:v.responseText,a)}};p&&(v.withCredentials=!0);C&&(v.responseType=C);v.send(k||null)}if(0<r)var Z=c(H,r);else r&&r.then&&r.then(H)}}function nd(){var b="{{",a="}}";this.startSymbol=function(a){return a?(b=a,this):b};this.endSymbol=function(b){return b?(a=b,this):a};this.$get=["$parse","$exceptionHandler", -"$sce",function(c,d,e){function f(f,k,l){for(var n,r,p=0,C=[],H=f.length,q=!1,A=[];p<H;)-1!=(n=f.indexOf(b,p))&&-1!=(r=f.indexOf(a,n+g))?(p!=n&&C.push(f.substring(p,n)),C.push(p=c(q=f.substring(n+g,r))),p.exp=q,p=r+h,q=!0):(p!=H&&C.push(f.substring(p)),p=H);(H=C.length)||(C.push(""),H=1);if(l&&1<C.length)throw jc("noconcat",f);if(!k||q)return A.length=H,p=function(a){try{for(var b=0,c=H,h;b<c;b++)"function"==typeof(h=C[b])&&(h=h(a),h=l?e.getTrusted(l,h):e.valueOf(h),null===h||x(h)?h="":"string"!= -typeof h&&(h=ma(h))),A[b]=h;return A.join("")}catch(g){a=jc("interr",f,g.toString()),d(a)}},p.exp=f,p.parts=C,p}var g=b.length,h=a.length;f.startSymbol=function(){return b};f.endSymbol=function(){return a};return f}]}function od(){this.$get=["$rootScope","$window","$q",function(b,a,c){function d(d,g,h,m){var k=a.setInterval,l=a.clearInterval,n=c.defer(),r=n.promise,p=0,C=z(m)&&!m;h=z(h)?h:0;r.then(null,null,d);r.$$intervalId=k(function(){n.notify(p++);0<h&&p>=h&&(n.resolve(p),l(r.$$intervalId),delete e[r.$$intervalId]); -C||b.$apply()},g);e[r.$$intervalId]=n;return r}var e={};d.cancel=function(a){return a&&a.$$intervalId in e?(e[a.$$intervalId].reject("canceled"),clearInterval(a.$$intervalId),delete e[a.$$intervalId],!0):!1};return d}]}function pd(){this.$get=function(){return{id:"en-us",NUMBER_FORMATS:{DECIMAL_SEP:".",GROUP_SEP:",",PATTERNS:[{minInt:1,minFrac:0,maxFrac:3,posPre:"",posSuf:"",negPre:"-",negSuf:"",gSize:3,lgSize:3},{minInt:1,minFrac:2,maxFrac:2,posPre:"\u00a4",posSuf:"",negPre:"(\u00a4",negSuf:")", -gSize:3,lgSize:3}],CURRENCY_SYM:"$"},DATETIME_FORMATS:{MONTH:"January February March April May June July August September October November December".split(" "),SHORTMONTH:"Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split(" "),DAY:"Sunday Monday Tuesday Wednesday Thursday Friday Saturday".split(" "),SHORTDAY:"Sun Mon Tue Wed Thu Fri Sat".split(" "),AMPMS:["AM","PM"],medium:"MMM d, y h:mm:ss a","short":"M/d/yy h:mm a",fullDate:"EEEE, MMMM d, y",longDate:"MMMM d, y",mediumDate:"MMM d, y",shortDate:"M/d/yy", -mediumTime:"h:mm:ss a",shortTime:"h:mm a"},pluralCat:function(b){return 1===b?"one":"other"}}}}function kc(b){b=b.split("/");for(var a=b.length;a--;)b[a]=sb(b[a]);return b.join("/")}function lc(b,a){var c=xa(b);a.$$protocol=c.protocol;a.$$host=c.hostname;a.$$port=U(c.port)||qd[c.protocol]||null}function mc(b,a){var c="/"!==b.charAt(0);c&&(b="/"+b);var d=xa(b);a.$$path=decodeURIComponent(c&&"/"===d.pathname.charAt(0)?d.pathname.substring(1):d.pathname);a.$$search=Ub(d.search);a.$$hash=decodeURIComponent(d.hash); -a.$$path&&"/"!=a.$$path.charAt(0)&&(a.$$path="/"+a.$$path)}function la(b,a){if(0===a.indexOf(b))return a.substr(b.length)}function Wa(b){var a=b.indexOf("#");return-1==a?b:b.substr(0,a)}function Fb(b){return b.substr(0,Wa(b).lastIndexOf("/")+1)}function nc(b,a){this.$$html5=!0;a=a||"";var c=Fb(b);lc(b,this);this.$$parse=function(a){var b=la(c,a);if(!D(b))throw Gb("ipthprfx",a,c);mc(b,this);this.$$path||(this.$$path="/");this.$$compose()};this.$$compose=function(){var a=Vb(this.$$search),b=this.$$hash? -"#"+sb(this.$$hash):"";this.$$url=kc(this.$$path)+(a?"?"+a:"")+b;this.$$absUrl=c+this.$$url.substr(1)};this.$$rewrite=function(d){var e;if((e=la(b,d))!==s)return d=e,(e=la(a,e))!==s?c+(la("/",e)||e):b+d;if((e=la(c,d))!==s)return c+e;if(c==d+"/")return c}}function Hb(b,a){var c=Fb(b);lc(b,this);this.$$parse=function(d){var e=la(b,d)||la(c,d),e="#"==e.charAt(0)?la(a,e):this.$$html5?e:"";if(!D(e))throw Gb("ihshprfx",d,a);mc(e,this);this.$$compose()};this.$$compose=function(){var c=Vb(this.$$search), -e=this.$$hash?"#"+sb(this.$$hash):"";this.$$url=kc(this.$$path)+(c?"?"+c:"")+e;this.$$absUrl=b+(this.$$url?a+this.$$url:"")};this.$$rewrite=function(a){if(Wa(b)==Wa(a))return a}}function oc(b,a){this.$$html5=!0;Hb.apply(this,arguments);var c=Fb(b);this.$$rewrite=function(d){var e;if(b==Wa(d))return d;if(e=la(c,d))return b+a+e;if(c===d+"/")return c}}function hb(b){return function(){return this[b]}}function pc(b,a){return function(c){if(x(c))return this[b];this[b]=a(c);this.$$compose();return this}} -function rd(){var b="",a=!1;this.hashPrefix=function(a){return z(a)?(b=a,this):b};this.html5Mode=function(b){return z(b)?(a=b,this):a};this.$get=["$rootScope","$browser","$sniffer","$rootElement",function(c,d,e,f){function g(a){c.$broadcast("$locationChangeSuccess",h.absUrl(),a)}var h,m=d.baseHref(),k=d.url();a?(m=k.substring(0,k.indexOf("/",k.indexOf("//")+2))+(m||"/"),e=e.history?nc:oc):(m=Wa(k),e=Hb);h=new e(m,"#"+b);h.$$parse(h.$$rewrite(k));f.on("click",function(a){if(!a.ctrlKey&&!a.metaKey&& -2!=a.which){for(var b=y(a.target);"a"!==w(b[0].nodeName);)if(b[0]===f[0]||!(b=b.parent())[0])return;var e=b.prop("href"),g=h.$$rewrite(e);e&&(!b.attr("target")&&g&&!a.isDefaultPrevented())&&(a.preventDefault(),g!=d.url()&&(h.$$parse(g),c.$apply(),W.angular["ff-684208-preventDefault"]=!0))}});h.absUrl()!=k&&d.url(h.absUrl(),!0);d.onUrlChange(function(a){h.absUrl()!=a&&(c.$broadcast("$locationChangeStart",a,h.absUrl()).defaultPrevented?d.url(h.absUrl()):(c.$evalAsync(function(){var b=h.absUrl();h.$$parse(a); -g(b)}),c.$$phase||c.$digest()))});var l=0;c.$watch(function(){var a=d.url(),b=h.$$replace;l&&a==h.absUrl()||(l++,c.$evalAsync(function(){c.$broadcast("$locationChangeStart",h.absUrl(),a).defaultPrevented?h.$$parse(a):(d.url(h.absUrl(),b),g(a))}));h.$$replace=!1;return l});return h}]}function sd(){var b=!0,a=this;this.debugEnabled=function(a){return z(a)?(b=a,this):b};this.$get=["$window",function(c){function d(a){a instanceof Error&&(a.stack?a=a.message&&-1===a.stack.indexOf(a.message)?"Error: "+ -a.message+"\n"+a.stack:a.stack:a.sourceURL&&(a=a.message+"\n"+a.sourceURL+":"+a.line));return a}function e(a){var b=c.console||{},e=b[a]||b.log||t;return e.apply?function(){var a=[];q(arguments,function(b){a.push(d(b))});return e.apply(b,a)}:function(a,b){e(a,null==b?"":b)}}return{log:e("log"),info:e("info"),warn:e("warn"),error:e("error"),debug:function(){var c=e("debug");return function(){b&&c.apply(a,arguments)}}()}}]}function ha(b,a,c){if("string"!==typeof b&&"[object String]"!==Ka.apply(b))return b; -if("constructor"===b&&!c)throw pa("isecfld",a);if("_"===b.charAt(0)||"_"===b.charAt(b.length-1))throw pa("isecprv",a);return b}function Xa(b,a){if(b&&b.constructor===b)throw pa("isecfn",a);if(b&&b.document&&b.location&&b.alert&&b.setInterval)throw pa("isecwindow",a);if(b&&(b.nodeName||b.on&&b.find))throw pa("isecdom",a);return b}function ib(b,a,c,d,e){e=e||{};a=a.split(".");for(var f,g=0;1<a.length;g++){f=ha(a.shift(),d);var h=b[f];h||(h={},b[f]=h);b=h;b.then&&e.unwrapPromises&&(qa(d),"$$v"in b|| -function(a){a.then(function(b){a.$$v=b})}(b),b.$$v===s&&(b.$$v={}),b=b.$$v)}f=ha(a.shift(),d);return b[f]=c}function qc(b,a,c,d,e,f,g){ha(b,f);ha(a,f);ha(c,f);ha(d,f);ha(e,f);return g.unwrapPromises?function(h,g){var k=g&&g.hasOwnProperty(b)?g:h,l;if(null===k||k===s)return k;(k=k[b])&&k.then&&(qa(f),"$$v"in k||(l=k,l.$$v=s,l.then(function(a){l.$$v=a})),k=k.$$v);if(!a||null===k||k===s)return k;(k=k[a])&&k.then&&(qa(f),"$$v"in k||(l=k,l.$$v=s,l.then(function(a){l.$$v=a})),k=k.$$v);if(!c||null===k|| -k===s)return k;(k=k[c])&&k.then&&(qa(f),"$$v"in k||(l=k,l.$$v=s,l.then(function(a){l.$$v=a})),k=k.$$v);if(!d||null===k||k===s)return k;(k=k[d])&&k.then&&(qa(f),"$$v"in k||(l=k,l.$$v=s,l.then(function(a){l.$$v=a})),k=k.$$v);if(!e||null===k||k===s)return k;(k=k[e])&&k.then&&(qa(f),"$$v"in k||(l=k,l.$$v=s,l.then(function(a){l.$$v=a})),k=k.$$v);return k}:function(f,g){var k=g&&g.hasOwnProperty(b)?g:f;if(null===k||k===s)return k;k=k[b];if(!a||null===k||k===s)return k;k=k[a];if(!c||null===k||k===s)return k; -k=k[c];if(!d||null===k||k===s)return k;k=k[d];return e&&null!==k&&k!==s?k=k[e]:k}}function rc(b,a,c){if(Ib.hasOwnProperty(b))return Ib[b];var d=b.split("."),e=d.length,f;if(a.csp)f=6>e?qc(d[0],d[1],d[2],d[3],d[4],c,a):function(b,f){var h=0,g;do g=qc(d[h++],d[h++],d[h++],d[h++],d[h++],c,a)(b,f),f=s,b=g;while(h<e);return g};else{var g="var l, fn, p;\n";q(d,function(b,d){ha(b,c);g+="if(s === null || s === undefined) return s;\nl=s;\ns="+(d?"s":'((k&&k.hasOwnProperty("'+b+'"))?k:s)')+'["'+b+'"];\n'+(a.unwrapPromises? -'if (s && s.then) {\n pw("'+c.replace(/\"/g,'\\"')+'");\n if (!("$$v" in s)) {\n p=s;\n p.$$v = undefined;\n p.then(function(v) {p.$$v=v;});\n}\n s=s.$$v\n}\n':"")});var g=g+"return s;",h=new Function("s","k","pw",g);h.toString=function(){return g};f=function(a,b){return h(a,b,qa)}}"hasOwnProperty"!==b&&(Ib[b]=f);return f}function td(){var b={},a={csp:!1,unwrapPromises:!1,logPromiseWarnings:!0};this.unwrapPromises=function(b){return z(b)?(a.unwrapPromises=!!b,this):a.unwrapPromises};this.logPromiseWarnings= -function(b){return z(b)?(a.logPromiseWarnings=b,this):a.logPromiseWarnings};this.$get=["$filter","$sniffer","$log",function(c,d,e){a.csp=d.csp;qa=function(b){a.logPromiseWarnings&&!sc.hasOwnProperty(b)&&(sc[b]=!0,e.warn("[$parse] Promise found in the expression `"+b+"`. Automatic unwrapping of promises in Angular expressions is deprecated."))};return function(d){var e;switch(typeof d){case "string":if(b.hasOwnProperty(d))return b[d];e=new Jb(a);e=(new Ya(e,c,a)).parse(d,!1);"hasOwnProperty"!==d&& -(b[d]=e);return e;case "function":return d;default:return t}}}]}function ud(){this.$get=["$rootScope","$exceptionHandler",function(b,a){return vd(function(a){b.$evalAsync(a)},a)}]}function vd(b,a){function c(a){return a}function d(a){return g(a)}var e=function(){var h=[],m,k;return k={resolve:function(a){if(h){var c=h;h=s;m=f(a);c.length&&b(function(){for(var a,b=0,d=c.length;b<d;b++)a=c[b],m.then(a[0],a[1],a[2])})}},reject:function(a){k.resolve(g(a))},notify:function(a){if(h){var c=h;h.length&&b(function(){for(var b, -d=0,e=c.length;d<e;d++)b=c[d],b[2](a)})}},promise:{then:function(b,f,g){var k=e(),C=function(d){try{k.resolve((B(b)?b:c)(d))}catch(e){k.reject(e),a(e)}},H=function(b){try{k.resolve((B(f)?f:d)(b))}catch(c){k.reject(c),a(c)}},q=function(b){try{k.notify((B(g)?g:c)(b))}catch(d){a(d)}};h?h.push([C,H,q]):m.then(C,H,q);return k.promise},"catch":function(a){return this.then(null,a)},"finally":function(a){function b(a,c){var d=e();c?d.resolve(a):d.reject(a);return d.promise}function d(e,f){var h=null;try{h= -(a||c)()}catch(g){return b(g,!1)}return h&&B(h.then)?h.then(function(){return b(e,f)},function(a){return b(a,!1)}):b(e,f)}return this.then(function(a){return d(a,!0)},function(a){return d(a,!1)})}}}},f=function(a){return a&&B(a.then)?a:{then:function(c){var d=e();b(function(){d.resolve(c(a))});return d.promise}}},g=function(c){return{then:function(f,g){var l=e();b(function(){try{l.resolve((B(g)?g:d)(c))}catch(b){l.reject(b),a(b)}});return l.promise}}};return{defer:e,reject:g,when:function(h,m,k,l){var n= -e(),r,p=function(b){try{return(B(m)?m:c)(b)}catch(d){return a(d),g(d)}},C=function(b){try{return(B(k)?k:d)(b)}catch(c){return a(c),g(c)}},q=function(b){try{return(B(l)?l:c)(b)}catch(d){a(d)}};b(function(){f(h).then(function(a){r||(r=!0,n.resolve(f(a).then(p,C,q)))},function(a){r||(r=!0,n.resolve(C(a)))},function(a){r||n.notify(q(a))})});return n.promise},all:function(a){var b=e(),c=0,d=J(a)?[]:{};q(a,function(a,e){c++;f(a).then(function(a){d.hasOwnProperty(e)||(d[e]=a,--c||b.resolve(d))},function(a){d.hasOwnProperty(e)|| -b.reject(a)})});0===c&&b.resolve(d);return b.promise}}}function wd(){var b=10,a=L("$rootScope");this.digestTtl=function(a){arguments.length&&(b=a);return b};this.$get=["$injector","$exceptionHandler","$parse","$browser",function(c,d,e,f){function g(){this.$id=Za();this.$$phase=this.$parent=this.$$watchers=this.$$nextSibling=this.$$prevSibling=this.$$childHead=this.$$childTail=null;this["this"]=this.$root=this;this.$$destroyed=!1;this.$$asyncQueue=[];this.$$postDigestQueue=[];this.$$listeners={};this.$$isolateBindings= -{}}function h(b){if(l.$$phase)throw a("inprog",l.$$phase);l.$$phase=b}function m(a,b){var c=e(a);Oa(c,b);return c}function k(){}g.prototype={constructor:g,$new:function(a){a?(a=new g,a.$root=this.$root,a.$$asyncQueue=this.$$asyncQueue,a.$$postDigestQueue=this.$$postDigestQueue):(a=function(){},a.prototype=this,a=new a,a.$id=Za());a["this"]=a;a.$$listeners={};a.$parent=this;a.$$watchers=a.$$nextSibling=a.$$childHead=a.$$childTail=null;a.$$prevSibling=this.$$childTail;this.$$childHead?this.$$childTail= -this.$$childTail.$$nextSibling=a:this.$$childHead=this.$$childTail=a;return a},$watch:function(a,b,c){var d=m(a,"watch"),e=this.$$watchers,f={fn:b,last:k,get:d,exp:a,eq:!!c};if(!B(b)){var h=m(b||t,"listener");f.fn=function(a,b,c){h(c)}}if("string"==typeof a&&d.constant){var g=f.fn;f.fn=function(a,b,c){g.call(this,a,b,c);La(e,f)}}e||(e=this.$$watchers=[]);e.unshift(f);return function(){La(e,f)}},$watchCollection:function(a,b){var c=this,d,f,h=0,g=e(a),k=[],l={},m=0;return this.$watch(function(){f= -g(c);var a,b;if(T(f))if(pb(f))for(d!==k&&(d=k,m=d.length=0,h++),a=f.length,m!==a&&(h++,d.length=m=a),b=0;b<a;b++)d[b]!==f[b]&&(h++,d[b]=f[b]);else{d!==l&&(d=l={},m=0,h++);a=0;for(b in f)f.hasOwnProperty(b)&&(a++,d.hasOwnProperty(b)?d[b]!==f[b]&&(h++,d[b]=f[b]):(m++,d[b]=f[b],h++));if(m>a)for(b in h++,d)d.hasOwnProperty(b)&&!f.hasOwnProperty(b)&&(m--,delete d[b])}else d!==f&&(d=f,h++);return h},function(){b(f,d,c)})},$digest:function(){var c,e,f,g,m=this.$$asyncQueue,q=this.$$postDigestQueue,s,t,G= -b,v,y=[],E,z,$;h("$digest");do{t=!1;for(v=this;m.length;)try{$=m.shift(),$.scope.$eval($.expression)}catch(x){d(x)}do{if(g=v.$$watchers)for(s=g.length;s--;)try{(c=g[s])&&((e=c.get(v))!==(f=c.last)&&!(c.eq?Aa(e,f):"number"==typeof e&&"number"==typeof f&&isNaN(e)&&isNaN(f)))&&(t=!0,c.last=c.eq?da(e):e,c.fn(e,f===k?e:f,v),5>G&&(E=4-G,y[E]||(y[E]=[]),z=B(c.exp)?"fn: "+(c.exp.name||c.exp.toString()):c.exp,z+="; newVal: "+ma(e)+"; oldVal: "+ma(f),y[E].push(z)))}catch(M){d(M)}if(!(g=v.$$childHead||v!==this&& -v.$$nextSibling))for(;v!==this&&!(g=v.$$nextSibling);)v=v.$parent}while(v=g);if(t&&!G--)throw l.$$phase=null,a("infdig",b,ma(y));}while(t||m.length);for(l.$$phase=null;q.length;)try{q.shift()()}catch(w){d(w)}},$destroy:function(){if(l!=this&&!this.$$destroyed){var a=this.$parent;this.$broadcast("$destroy");this.$$destroyed=!0;a.$$childHead==this&&(a.$$childHead=this.$$nextSibling);a.$$childTail==this&&(a.$$childTail=this.$$prevSibling);this.$$prevSibling&&(this.$$prevSibling.$$nextSibling=this.$$nextSibling); -this.$$nextSibling&&(this.$$nextSibling.$$prevSibling=this.$$prevSibling);this.$parent=this.$$nextSibling=this.$$prevSibling=this.$$childHead=this.$$childTail=null}},$eval:function(a,b){return e(a)(this,b)},$evalAsync:function(a){l.$$phase||l.$$asyncQueue.length||f.defer(function(){l.$$asyncQueue.length&&l.$digest()});this.$$asyncQueue.push({scope:this,expression:a})},$$postDigest:function(a){this.$$postDigestQueue.push(a)},$apply:function(a){try{return h("$apply"),this.$eval(a)}catch(b){d(b)}finally{l.$$phase= -null;try{l.$digest()}catch(c){throw d(c),c;}}},$on:function(a,b){var c=this.$$listeners[a];c||(this.$$listeners[a]=c=[]);c.push(b);return function(){c[ab(c,b)]=null}},$emit:function(a,b){var c=[],e,f=this,h=!1,g={name:a,targetScope:f,stopPropagation:function(){h=!0},preventDefault:function(){g.defaultPrevented=!0},defaultPrevented:!1},k=[g].concat(ta.call(arguments,1)),l,m;do{e=f.$$listeners[a]||c;g.currentScope=f;l=0;for(m=e.length;l<m;l++)if(e[l])try{e[l].apply(null,k)}catch(q){d(q)}else e.splice(l, -1),l--,m--;if(h)break;f=f.$parent}while(f);return g},$broadcast:function(a,b){var c=this,e=this,f={name:a,targetScope:this,preventDefault:function(){f.defaultPrevented=!0},defaultPrevented:!1},h=[f].concat(ta.call(arguments,1)),g,k;do{c=e;f.currentScope=c;e=c.$$listeners[a]||[];g=0;for(k=e.length;g<k;g++)if(e[g])try{e[g].apply(null,h)}catch(l){d(l)}else e.splice(g,1),g--,k--;if(!(e=c.$$childHead||c!==this&&c.$$nextSibling))for(;c!==this&&!(e=c.$$nextSibling);)c=c.$parent}while(c=e);return f}};var l= -new g;return l}]}function xd(b){if("self"===b)return b;if(D(b)){if(-1<b.indexOf("***"))throw ra("iwcard",b);b=b.replace(/([-()\[\]{}+?*.$\^|,:#<!\\])/g,"\\$1").replace(/\x08/g,"\\x08").replace("\\*\\*",".*").replace("\\*","[^:/.?&;]*");return RegExp("^"+b+"$")}if($a(b))return RegExp("^"+b.source+"$");throw ra("imatcher");}function tc(b){var a=[];z(b)&&q(b,function(b){a.push(xd(b))});return a}function yd(){this.SCE_CONTEXTS=ca;var b=["self"],a=[];this.resourceUrlWhitelist=function(a){arguments.length&& -(b=tc(a));return b};this.resourceUrlBlacklist=function(b){arguments.length&&(a=tc(b));return a};this.$get=["$log","$document","$injector",function(c,d,e){function f(a){var b=function(a){this.$$unwrapTrustedValue=function(){return a}};a&&(b.prototype=new a);b.prototype.valueOf=function(){return this.$$unwrapTrustedValue()};b.prototype.toString=function(){return this.$$unwrapTrustedValue().toString()};return b}var g=function(a){throw ra("unsafe");};e.has("$sanitize")&&(g=e.get("$sanitize"));var h=f(), -m={};m[ca.HTML]=f(h);m[ca.CSS]=f(h);m[ca.URL]=f(h);m[ca.JS]=f(h);m[ca.RESOURCE_URL]=f(m[ca.URL]);return{trustAs:function(a,b){var c=m.hasOwnProperty(a)?m[a]:null;if(!c)throw ra("icontext",a,b);if(null===b||b===s||""===b)return b;if("string"!==typeof b)throw ra("itype",a);return new c(b)},getTrusted:function(c,d){if(null===d||d===s||""===d)return d;var e=m.hasOwnProperty(c)?m[c]:null;if(e&&d instanceof e)return d.$$unwrapTrustedValue();if(c===ca.RESOURCE_URL){var e=xa(d.toString()),f,h,q=!1;f=0;for(h= -b.length;f<h;f++)if("self"===b[f]?Eb(e):b[f].exec(e.href)){q=!0;break}if(q)for(f=0,h=a.length;f<h;f++)if("self"===a[f]?Eb(e):a[f].exec(e.href)){q=!1;break}if(q)return d;throw ra("insecurl",d.toString());}if(c===ca.HTML)return g(d);throw ra("unsafe");},valueOf:function(a){return a instanceof h?a.$$unwrapTrustedValue():a}}}]}function zd(){var b=!0;this.enabled=function(a){arguments.length&&(b=!!a);return b};this.$get=["$parse","$document","$sceDelegate",function(a,c,d){if(b&&P&&(c=c[0].documentMode, -c!==s&&8>c))throw ra("iequirks");var e=da(ca);e.isEnabled=function(){return b};e.trustAs=d.trustAs;e.getTrusted=d.getTrusted;e.valueOf=d.valueOf;b||(e.trustAs=e.getTrusted=function(a,b){return b},e.valueOf=za);e.parseAs=function(b,c){var d=a(c);return d.literal&&d.constant?d:function(a,c){return e.getTrusted(b,d(a,c))}};var f=e.parseAs,g=e.getTrusted,h=e.trustAs;q(ca,function(a,b){var c=w(b);e[Pa("parse_as_"+c)]=function(b){return f(a,b)};e[Pa("get_trusted_"+c)]=function(b){return g(a,b)};e[Pa("trust_as_"+ -c)]=function(b){return h(a,b)}});return e}]}function Ad(){this.$get=["$window","$document",function(b,a){var c={},d=U((/android (\d+)/.exec(w((b.navigator||{}).userAgent))||[])[1]),e=/Boxee/i.test((b.navigator||{}).userAgent),f=a[0]||{},g,h=/^(Moz|webkit|O|ms)(?=[A-Z])/,m=f.body&&f.body.style,k=!1,l=!1;if(m){for(var n in m)if(k=h.exec(n)){g=k[0];g=g.substr(0,1).toUpperCase()+g.substr(1);break}g||(g="WebkitOpacity"in m&&"webkit");k=!!("transition"in m||g+"Transition"in m);l=!!("animation"in m||g+"Animation"in -m);!d||k&&l||(k=D(f.body.style.webkitTransition),l=D(f.body.style.webkitAnimation))}return{history:!(!b.history||!b.history.pushState||4>d||e),hashchange:"onhashchange"in b&&(!f.documentMode||7<f.documentMode),hasEvent:function(a){if("input"==a&&9==P)return!1;if(x(c[a])){var b=f.createElement("div");c[a]="on"+a in b}return c[a]},csp:Rb(),vendorPrefix:g,transitions:k,animations:l,msie:P}}]}function Bd(){this.$get=["$rootScope","$browser","$q","$exceptionHandler",function(b,a,c,d){function e(e,h,m){var k= -c.defer(),l=k.promise,n=z(m)&&!m;h=a.defer(function(){try{k.resolve(e())}catch(a){k.reject(a),d(a)}finally{delete f[l.$$timeoutId]}n||b.$apply()},h);l.$$timeoutId=h;f[h]=k;return l}var f={};e.cancel=function(b){return b&&b.$$timeoutId in f?(f[b.$$timeoutId].reject("canceled"),delete f[b.$$timeoutId],a.defer.cancel(b.$$timeoutId)):!1};return e}]}function xa(b){P&&(R.setAttribute("href",b),b=R.href);R.setAttribute("href",b);return{href:R.href,protocol:R.protocol?R.protocol.replace(/:$/,""):"",host:R.host, -search:R.search?R.search.replace(/^\?/,""):"",hash:R.hash?R.hash.replace(/^#/,""):"",hostname:R.hostname,port:R.port,pathname:R.pathname&&"/"===R.pathname.charAt(0)?R.pathname:"/"+R.pathname}}function Eb(b){b=D(b)?xa(b):b;return b.protocol===uc.protocol&&b.host===uc.host}function Cd(){this.$get=aa(W)}function vc(b){function a(d,e){if(T(d)){var f={};q(d,function(b,c){f[c]=a(c,b)});return f}return b.factory(d+c,e)}var c="Filter";this.register=a;this.$get=["$injector",function(a){return function(b){return a.get(b+ -c)}}];a("currency",wc);a("date",xc);a("filter",Dd);a("json",Ed);a("limitTo",Fd);a("lowercase",Gd);a("number",yc);a("orderBy",zc);a("uppercase",Hd)}function Dd(){return function(b,a,c){if(!J(b))return b;var d=typeof c,e=[];e.check=function(a){for(var b=0;b<e.length;b++)if(!e[b](a))return!1;return!0};"function"!==d&&(c="boolean"===d&&c?function(a,b){return bb.equals(a,b)}:function(a,b){b=(""+b).toLowerCase();return-1<(""+a).toLowerCase().indexOf(b)});var f=function(a,b){if("string"==typeof b&&"!"=== -b.charAt(0))return!f(a,b.substr(1));switch(typeof a){case "boolean":case "number":case "string":return c(a,b);case "object":switch(typeof b){case "object":return c(a,b);default:for(var d in a)if("$"!==d.charAt(0)&&f(a[d],b))return!0}return!1;case "array":for(d=0;d<a.length;d++)if(f(a[d],b))return!0;return!1;default:return!1}};switch(typeof a){case "boolean":case "number":case "string":a={$:a};case "object":for(var g in a)"$"==g?function(){if(a[g]){var b=g;e.push(function(c){return f(c,a[b])})}}(): -function(){if("undefined"!=typeof a[g]){var b=g;e.push(function(c){return f(ub(c,b),a[b])})}}();break;case "function":e.push(a);break;default:return b}for(var d=[],h=0;h<b.length;h++){var m=b[h];e.check(m)&&d.push(m)}return d}}function wc(b){var a=b.NUMBER_FORMATS;return function(b,d){x(d)&&(d=a.CURRENCY_SYM);return Ac(b,a.PATTERNS[1],a.GROUP_SEP,a.DECIMAL_SEP,2).replace(/\u00A4/g,d)}}function yc(b){var a=b.NUMBER_FORMATS;return function(b,d){return Ac(b,a.PATTERNS[0],a.GROUP_SEP,a.DECIMAL_SEP,d)}} -function Ac(b,a,c,d,e){if(isNaN(b)||!isFinite(b))return"";var f=0>b;b=Math.abs(b);var g=b+"",h="",m=[],k=!1;if(-1!==g.indexOf("e")){var l=g.match(/([\d\.]+)e(-?)(\d+)/);l&&"-"==l[2]&&l[3]>e+1?g="0":(h=g,k=!0)}if(k)0<e&&(-1<b&&1>b)&&(h=b.toFixed(e));else{g=(g.split(Bc)[1]||"").length;x(e)&&(e=Math.min(Math.max(a.minFrac,g),a.maxFrac));g=Math.pow(10,e);b=Math.round(b*g)/g;b=(""+b).split(Bc);g=b[0];b=b[1]||"";var l=0,n=a.lgSize,r=a.gSize;if(g.length>=n+r)for(l=g.length-n,k=0;k<l;k++)0===(l-k)%r&&0!== -k&&(h+=c),h+=g.charAt(k);for(k=l;k<g.length;k++)0===(g.length-k)%n&&0!==k&&(h+=c),h+=g.charAt(k);for(;b.length<e;)b+="0";e&&"0"!==e&&(h+=d+b.substr(0,e))}m.push(f?a.negPre:a.posPre);m.push(h);m.push(f?a.negSuf:a.posSuf);return m.join("")}function Kb(b,a,c){var d="";0>b&&(d="-",b=-b);for(b=""+b;b.length<a;)b="0"+b;c&&(b=b.substr(b.length-a));return d+b}function V(b,a,c,d){c=c||0;return function(e){e=e["get"+b]();if(0<c||e>-c)e+=c;0===e&&-12==c&&(e=12);return Kb(e,a,d)}}function jb(b,a){return function(c, -d){var e=c["get"+b](),f=Ga(a?"SHORT"+b:b);return d[f][e]}}function xc(b){function a(a){var b;if(b=a.match(c)){a=new Date(0);var f=0,g=0,h=b[8]?a.setUTCFullYear:a.setFullYear,m=b[8]?a.setUTCHours:a.setHours;b[9]&&(f=U(b[9]+b[10]),g=U(b[9]+b[11]));h.call(a,U(b[1]),U(b[2])-1,U(b[3]));f=U(b[4]||0)-f;g=U(b[5]||0)-g;h=U(b[6]||0);b=Math.round(1E3*parseFloat("0."+(b[7]||0)));m.call(a,f,g,h,b)}return a}var c=/^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(Z|([+-])(\d\d):?(\d\d))?)?$/; -return function(c,e){var f="",g=[],h,m;e=e||"mediumDate";e=b.DATETIME_FORMATS[e]||e;D(c)&&(c=Id.test(c)?U(c):a(c));qb(c)&&(c=new Date(c));if(!Ja(c))return c;for(;e;)(m=Jd.exec(e))?(g=g.concat(ta.call(m,1)),e=g.pop()):(g.push(e),e=null);q(g,function(a){h=Kd[a];f+=h?h(c,b.DATETIME_FORMATS):a.replace(/(^'|'$)/g,"").replace(/''/g,"'")});return f}}function Ed(){return function(b){return ma(b,!0)}}function Fd(){return function(b,a){if(!J(b)&&!D(b))return b;a=U(a);if(D(b))return a?0<=a?b.slice(0,a):b.slice(a, -b.length):"";var c=[],d,e;a>b.length?a=b.length:a<-b.length&&(a=-b.length);0<a?(d=0,e=a):(d=b.length+a,e=b.length);for(;d<e;d++)c.push(b[d]);return c}}function zc(b){return function(a,c,d){function e(a,b){return Na(b)?function(b,c){return a(c,b)}:a}if(!J(a)||!c)return a;c=J(c)?c:[c];c=Lc(c,function(a){var c=!1,d=a||za;if(D(a)){if("+"==a.charAt(0)||"-"==a.charAt(0))c="-"==a.charAt(0),a=a.substring(1);d=b(a)}return e(function(a,b){var c;c=d(a);var e=d(b),f=typeof c,g=typeof e;f==g?("string"==f&&(c= -c.toLowerCase(),e=e.toLowerCase()),c=c===e?0:c<e?-1:1):c=f<g?-1:1;return c},c)});for(var f=[],g=0;g<a.length;g++)f.push(a[g]);return f.sort(e(function(a,b){for(var d=0;d<c.length;d++){var e=c[d](a,b);if(0!==e)return e}return 0},d))}}function sa(b){B(b)&&(b={link:b});b.restrict=b.restrict||"AC";return aa(b)}function Cc(b,a){function c(a,c){c=c?"-"+cb(c,"-"):"";b.removeClass((a?kb:lb)+c).addClass((a?lb:kb)+c)}var d=this,e=b.parent().controller("form")||mb,f=0,g=d.$error={},h=[];d.$name=a.name||a.ngForm; -d.$dirty=!1;d.$pristine=!0;d.$valid=!0;d.$invalid=!1;e.$addControl(d);b.addClass(Ha);c(!0);d.$addControl=function(a){na(a.$name,"input");h.push(a);a.$name&&(d[a.$name]=a)};d.$removeControl=function(a){a.$name&&d[a.$name]===a&&delete d[a.$name];q(g,function(b,c){d.$setValidity(c,!0,a)});La(h,a)};d.$setValidity=function(a,b,h){var n=g[a];if(b)n&&(La(n,h),n.length||(f--,f||(c(b),d.$valid=!0,d.$invalid=!1),g[a]=!1,c(!0,a),e.$setValidity(a,!0,d)));else{f||c(b);if(n){if(-1!=ab(n,h))return}else g[a]=n=[], -f++,c(!1,a),e.$setValidity(a,!1,d);n.push(h);d.$valid=!1;d.$invalid=!0}};d.$setDirty=function(){b.removeClass(Ha).addClass(nb);d.$dirty=!0;d.$pristine=!1;e.$setDirty()};d.$setPristine=function(){b.removeClass(nb).addClass(Ha);d.$dirty=!1;d.$pristine=!0;q(h,function(a){a.$setPristine()})}}function ob(b,a,c,d,e,f){var g=function(){var e=a.val();Na(c.ngTrim||"T")&&(e=Y(e));d.$viewValue!==e&&b.$apply(function(){d.$setViewValue(e)})};if(e.hasEvent("input"))a.on("input",g);else{var h,m=function(){h||(h= -f.defer(function(){g();h=null}))};a.on("keydown",function(a){a=a.keyCode;91===a||(15<a&&19>a||37<=a&&40>=a)||m()});a.on("change",g);if(e.hasEvent("paste"))a.on("paste cut",m)}d.$render=function(){a.val(d.$isEmpty(d.$viewValue)?"":d.$viewValue)};var k=c.ngPattern,l=function(a,b){if(d.$isEmpty(b)||a.test(b))return d.$setValidity("pattern",!0),b;d.$setValidity("pattern",!1);return s};k&&((e=k.match(/^\/(.*)\/([gim]*)$/))?(k=RegExp(e[1],e[2]),e=function(a){return l(k,a)}):e=function(c){var d=b.$eval(k); -if(!d||!d.test)throw L("ngPattern")("noregexp",k,d,ea(a));return l(d,c)},d.$formatters.push(e),d.$parsers.push(e));if(c.ngMinlength){var n=U(c.ngMinlength);e=function(a){if(!d.$isEmpty(a)&&a.length<n)return d.$setValidity("minlength",!1),s;d.$setValidity("minlength",!0);return a};d.$parsers.push(e);d.$formatters.push(e)}if(c.ngMaxlength){var r=U(c.ngMaxlength);e=function(a){if(!d.$isEmpty(a)&&a.length>r)return d.$setValidity("maxlength",!1),s;d.$setValidity("maxlength",!0);return a};d.$parsers.push(e); -d.$formatters.push(e)}}function Lb(b,a){b="ngClass"+b;return function(){return{restrict:"AC",link:function(c,d,e){function f(b){if(!0===a||c.$index%2===a)h&&!Aa(b,h)&&e.$removeClass(g(h)),e.$addClass(g(b));h=da(b)}function g(a){if(J(a))return a.join(" ");if(T(a)){var b=[];q(a,function(a,c){a&&b.push(c)});return b.join(" ")}return a}var h;c.$watch(e[b],f,!0);e.$observe("class",function(a){f(c.$eval(e[b]))});"ngClass"!==b&&c.$watch("$index",function(d,f){var h=d&1;h!==f&1&&(h===a?(h=c.$eval(e[b]),e.$addClass(g(h))): -(h=c.$eval(e[b]),e.$removeClass(g(h))))})}}}}var w=function(b){return D(b)?b.toLowerCase():b},Ga=function(b){return D(b)?b.toUpperCase():b},P,y,Ba,ta=[].slice,Ld=[].push,Ka=Object.prototype.toString,Ma=L("ng"),bb=W.angular||(W.angular={}),Ua,Ea,ia=["0","0","0"];P=U((/msie (\d+)/.exec(w(navigator.userAgent))||[])[1]);isNaN(P)&&(P=U((/trident\/.*; rv:(\d+)/.exec(w(navigator.userAgent))||[])[1]));t.$inject=[];za.$inject=[];var Y=function(){return String.prototype.trim?function(b){return D(b)?b.trim(): -b}:function(b){return D(b)?b.replace(/^\s*/,"").replace(/\s*$/,""):b}}();Ea=9>P?function(b){b=b.nodeName?b:b[0];return b.scopeName&&"HTML"!=b.scopeName?Ga(b.scopeName+":"+b.nodeName):b.nodeName}:function(b){return b.nodeName?b.nodeName:b[0].nodeName};var Pc=/[A-Z]/g,Md={full:"1.2.0",major:1,minor:"NG_VERSION_MINOR",dot:0,codeName:"timely-delivery"},Ra=Q.cache={},db=Q.expando="ng-"+(new Date).getTime(),Tc=1,Dc=W.document.addEventListener?function(b,a,c){b.addEventListener(a,c,!1)}:function(b,a,c){b.attachEvent("on"+ -a,c)},Ab=W.document.removeEventListener?function(b,a,c){b.removeEventListener(a,c,!1)}:function(b,a,c){b.detachEvent("on"+a,c)},Rc=/([\:\-\_]+(.))/g,Sc=/^moz([A-Z])/,xb=L("jqLite"),Da=Q.prototype={ready:function(b){function a(){c||(c=!0,b())}var c=!1;"complete"===O.readyState?setTimeout(a):(this.on("DOMContentLoaded",a),Q(W).on("load",a))},toString:function(){var b=[];q(this,function(a){b.push(""+a)});return"["+b.join(", ")+"]"},eq:function(b){return 0<=b?y(this[b]):y(this[this.length+b])},length:0, -push:Ld,sort:[].sort,splice:[].splice},fb={};q("multiple selected checked disabled readOnly required open".split(" "),function(b){fb[w(b)]=b});var cc={};q("input select option textarea button form details".split(" "),function(b){cc[Ga(b)]=!0});q({data:$b,inheritedData:eb,scope:function(b){return y(b).data("$scope")||eb(b.parentNode||b,["$isolateScope","$scope"])},isolateScope:function(b){return y(b).data("$isolateScope")||y(b).data("$isolateScopeNoTemplate")},controller:ac,injector:function(b){return eb(b, -"$injector")},removeAttr:function(b,a){b.removeAttribute(a)},hasClass:Bb,css:function(b,a,c){a=Pa(a);if(z(c))b.style[a]=c;else{var d;8>=P&&(d=b.currentStyle&&b.currentStyle[a],""===d&&(d="auto"));d=d||b.style[a];8>=P&&(d=""===d?s:d);return d}},attr:function(b,a,c){var d=w(a);if(fb[d])if(z(c))c?(b[a]=!0,b.setAttribute(a,d)):(b[a]=!1,b.removeAttribute(d));else return b[a]||(b.attributes.getNamedItem(a)||t).specified?d:s;else if(z(c))b.setAttribute(a,c);else if(b.getAttribute)return b=b.getAttribute(a, -2),null===b?s:b},prop:function(b,a,c){if(z(c))b[a]=c;else return b[a]},text:function(){function b(b,d){var e=a[b.nodeType];if(x(d))return e?b[e]:"";b[e]=d}var a=[];9>P?(a[1]="innerText",a[3]="nodeValue"):a[1]=a[3]="textContent";b.$dv="";return b}(),val:function(b,a){if(x(a)){if("SELECT"===Ea(b)&&b.multiple){var c=[];q(b.options,function(a){a.selected&&c.push(a.value||a.text)});return 0===c.length?null:c}return b.value}b.value=a},html:function(b,a){if(x(a))return b.innerHTML;for(var c=0,d=b.childNodes;c< -d.length;c++)Qa(d[c]);b.innerHTML=a}},function(b,a){Q.prototype[a]=function(a,d){var e,f;if((2==b.length&&b!==Bb&&b!==ac?a:d)===s){if(T(a)){for(e=0;e<this.length;e++)if(b===$b)b(this[e],a);else for(f in a)b(this[e],f,a[f]);return this}e=b.$dv;f=e===s?Math.min(this.length,1):this.length;for(var g=0;g<f;g++){var h=b(this[g],a,d);e=e?e+h:h}return e}for(e=0;e<this.length;e++)b(this[e],a,d);return this}});q({removeData:Yb,dealoc:Qa,on:function a(c,d,e,f){if(z(f))throw xb("onargs");var g=ja(c,"events"), -h=ja(c,"handle");g||ja(c,"events",g={});h||ja(c,"handle",h=Uc(c,g));q(d.split(" "),function(d){var f=g[d];if(!f){if("mouseenter"==d||"mouseleave"==d){var l=O.body.contains||O.body.compareDocumentPosition?function(a,c){var d=9===a.nodeType?a.documentElement:a,e=c&&c.parentNode;return a===e||!!(e&&1===e.nodeType&&(d.contains?d.contains(e):a.compareDocumentPosition&&a.compareDocumentPosition(e)&16))}:function(a,c){if(c)for(;c=c.parentNode;)if(c===a)return!0;return!1};g[d]=[];a(c,{mouseleave:"mouseout", -mouseenter:"mouseover"}[d],function(a){var c=a.relatedTarget;c&&(c===this||l(this,c))||h(a,d)})}else Dc(c,d,h),g[d]=[];f=g[d]}f.push(e)})},off:Zb,replaceWith:function(a,c){var d,e=a.parentNode;Qa(a);q(new Q(c),function(c){d?e.insertBefore(c,d.nextSibling):e.replaceChild(c,a);d=c})},children:function(a){var c=[];q(a.childNodes,function(a){1===a.nodeType&&c.push(a)});return c},contents:function(a){return a.childNodes||[]},append:function(a,c){q(new Q(c),function(c){1!==a.nodeType&&11!==a.nodeType|| -a.appendChild(c)})},prepend:function(a,c){if(1===a.nodeType){var d=a.firstChild;q(new Q(c),function(c){a.insertBefore(c,d)})}},wrap:function(a,c){c=y(c)[0];var d=a.parentNode;d&&d.replaceChild(c,a);c.appendChild(a)},remove:function(a){Qa(a);var c=a.parentNode;c&&c.removeChild(a)},after:function(a,c){var d=a,e=a.parentNode;q(new Q(c),function(a){e.insertBefore(a,d.nextSibling);d=a})},addClass:Db,removeClass:Cb,toggleClass:function(a,c,d){x(d)&&(d=!Bb(a,c));(d?Db:Cb)(a,c)},parent:function(a){return(a= -a.parentNode)&&11!==a.nodeType?a:null},next:function(a){if(a.nextElementSibling)return a.nextElementSibling;for(a=a.nextSibling;null!=a&&1!==a.nodeType;)a=a.nextSibling;return a},find:function(a,c){return a.getElementsByTagName(c)},clone:zb,triggerHandler:function(a,c,d){c=(ja(a,"events")||{})[c];d=d||[];var e=[{preventDefault:t,stopPropagation:t}];q(c,function(c){c.apply(a,e.concat(d))})}},function(a,c){Q.prototype[c]=function(c,e,f){for(var g,h=0;h<this.length;h++)x(g)?(g=a(this[h],c,e,f),z(g)&& -(g=y(g))):yb(g,a(this[h],c,e,f));return z(g)?g:this};Q.prototype.bind=Q.prototype.on;Q.prototype.unbind=Q.prototype.off});Sa.prototype={put:function(a,c){this[Ca(a)]=c},get:function(a){return this[Ca(a)]},remove:function(a){var c=this[a=Ca(a)];delete this[a];return c}};var Wc=/^function\s*[^\(]*\(\s*([^\)]*)\)/m,Xc=/,/,Yc=/^\s*(_?)(\S+?)\1\s*$/,Vc=/((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg,Ta=L("$injector"),Nd=L("$animate"),Od=["$provide",function(a){this.$$selectors={};this.register=function(c,d){var e= -c+"-animation";if(c&&"."!=c.charAt(0))throw Nd("notcsel",c);this.$$selectors[c.substr(1)]=e;a.factory(e,d)};this.$get=["$timeout",function(a){return{enter:function(d,e,f,g){f=f&&f[f.length-1];var h=e&&e[0]||f&&f.parentNode,m=f&&f.nextSibling||null;q(d,function(a){h.insertBefore(a,m)});g&&a(g,0,!1)},leave:function(d,e){d.remove();e&&a(e,0,!1)},move:function(a,c,f,g){this.enter(a,c,f,g)},addClass:function(d,e,f){e=D(e)?e:J(e)?e.join(" "):"";q(d,function(a){Db(a,e)});f&&a(f,0,!1)},removeClass:function(d, -e,f){e=D(e)?e:J(e)?e.join(" "):"";q(d,function(a){Cb(a,e)});f&&a(f,0,!1)},enabled:t}}]}],fa=L("$compile");ec.$inject=["$provide"];var fd=/^(x[\:\-_]|data[\:\-_])/i,md=W.XMLHttpRequest||function(){try{return new ActiveXObject("Msxml2.XMLHTTP.6.0")}catch(a){}try{return new ActiveXObject("Msxml2.XMLHTTP.3.0")}catch(c){}try{return new ActiveXObject("Msxml2.XMLHTTP")}catch(d){}throw L("$httpBackend")("noxhr");},jc=L("$interpolate"),Pd=/^([^\?#]*)(\?([^#]*))?(#(.*))?$/,qd={http:80,https:443,ftp:21},Gb= -L("$location");oc.prototype=Hb.prototype=nc.prototype={$$html5:!1,$$replace:!1,absUrl:hb("$$absUrl"),url:function(a,c){if(x(a))return this.$$url;var d=Pd.exec(a);d[1]&&this.path(decodeURIComponent(d[1]));(d[2]||d[1])&&this.search(d[3]||"");this.hash(d[5]||"",c);return this},protocol:hb("$$protocol"),host:hb("$$host"),port:hb("$$port"),path:pc("$$path",function(a){return"/"==a.charAt(0)?a:"/"+a}),search:function(a,c){switch(arguments.length){case 0:return this.$$search;case 1:if(D(a))this.$$search= -Ub(a);else if(T(a))this.$$search=a;else throw Gb("isrcharg");break;default:x(c)||null===c?delete this.$$search[a]:this.$$search[a]=c}this.$$compose();return this},hash:pc("$$hash",za),replace:function(){this.$$replace=!0;return this}};var pa=L("$parse"),sc={},qa,Ia={"null":function(){return null},"true":function(){return!0},"false":function(){return!1},undefined:t,"+":function(a,c,d,e){d=d(a,c);e=e(a,c);return z(d)?z(e)?d+e:d:z(e)?e:s},"-":function(a,c,d,e){d=d(a,c);e=e(a,c);return(z(d)?d:0)-(z(e)? -e:0)},"*":function(a,c,d,e){return d(a,c)*e(a,c)},"/":function(a,c,d,e){return d(a,c)/e(a,c)},"%":function(a,c,d,e){return d(a,c)%e(a,c)},"^":function(a,c,d,e){return d(a,c)^e(a,c)},"=":t,"===":function(a,c,d,e){return d(a,c)===e(a,c)},"!==":function(a,c,d,e){return d(a,c)!==e(a,c)},"==":function(a,c,d,e){return d(a,c)==e(a,c)},"!=":function(a,c,d,e){return d(a,c)!=e(a,c)},"<":function(a,c,d,e){return d(a,c)<e(a,c)},">":function(a,c,d,e){return d(a,c)>e(a,c)},"<=":function(a,c,d,e){return d(a,c)<= -e(a,c)},">=":function(a,c,d,e){return d(a,c)>=e(a,c)},"&&":function(a,c,d,e){return d(a,c)&&e(a,c)},"||":function(a,c,d,e){return d(a,c)||e(a,c)},"&":function(a,c,d,e){return d(a,c)&e(a,c)},"|":function(a,c,d,e){return e(a,c)(a,c,d(a,c))},"!":function(a,c,d){return!d(a,c)}},Qd={n:"\n",f:"\f",r:"\r",t:"\t",v:"\v","'":"'",'"':'"'},Jb=function(a){this.options=a};Jb.prototype={constructor:Jb,lex:function(a){this.text=a;this.index=0;this.ch=s;this.lastCh=":";this.tokens=[];var c;for(a=[];this.index<this.text.length;){this.ch= -this.text.charAt(this.index);if(this.is("\"'"))this.readString(this.ch);else if(this.isNumber(this.ch)||this.is(".")&&this.isNumber(this.peek()))this.readNumber();else if(this.isIdent(this.ch))this.readIdent(),this.was("{,")&&("{"===a[0]&&(c=this.tokens[this.tokens.length-1]))&&(c.json=-1===c.text.indexOf("."));else if(this.is("(){}[].,;:?"))this.tokens.push({index:this.index,text:this.ch,json:this.was(":[,")&&this.is("{[")||this.is("}]:,")}),this.is("{[")&&a.unshift(this.ch),this.is("}]")&&a.shift(), -this.index++;else if(this.isWhitespace(this.ch)){this.index++;continue}else{var d=this.ch+this.peek(),e=d+this.peek(2),f=Ia[this.ch],g=Ia[d],h=Ia[e];h?(this.tokens.push({index:this.index,text:e,fn:h}),this.index+=3):g?(this.tokens.push({index:this.index,text:d,fn:g}),this.index+=2):f?(this.tokens.push({index:this.index,text:this.ch,fn:f,json:this.was("[,:")&&this.is("+-")}),this.index+=1):this.throwError("Unexpected next character ",this.index,this.index+1)}this.lastCh=this.ch}return this.tokens}, -is:function(a){return-1!==a.indexOf(this.ch)},was:function(a){return-1!==a.indexOf(this.lastCh)},peek:function(a){a=a||1;return this.index+a<this.text.length?this.text.charAt(this.index+a):!1},isNumber:function(a){return"0"<=a&&"9">=a},isWhitespace:function(a){return" "===a||"\r"===a||"\t"===a||"\n"===a||"\v"===a||"\u00a0"===a},isIdent:function(a){return"a"<=a&&"z">=a||"A"<=a&&"Z">=a||"_"===a||"$"===a},isExpOperator:function(a){return"-"===a||"+"===a||this.isNumber(a)},throwError:function(a,c,d){d= -d||this.index;c=z(c)?"s "+c+"-"+this.index+" ["+this.text.substring(c,d)+"]":" "+d;throw pa("lexerr",a,c,this.text);},readNumber:function(){for(var a="",c=this.index;this.index<this.text.length;){var d=w(this.text.charAt(this.index));if("."==d||this.isNumber(d))a+=d;else{var e=this.peek();if("e"==d&&this.isExpOperator(e))a+=d;else if(this.isExpOperator(d)&&e&&this.isNumber(e)&&"e"==a.charAt(a.length-1))a+=d;else if(!this.isExpOperator(d)||e&&this.isNumber(e)||"e"!=a.charAt(a.length-1))break;else this.throwError("Invalid exponent")}this.index++}a*= -1;this.tokens.push({index:c,text:a,json:!0,fn:function(){return a}})},readIdent:function(){for(var a=this,c="",d=this.index,e,f,g,h;this.index<this.text.length;){h=this.text.charAt(this.index);if("."===h||this.isIdent(h)||this.isNumber(h))"."===h&&(e=this.index),c+=h;else break;this.index++}if(e)for(f=this.index;f<this.text.length;){h=this.text.charAt(f);if("("===h){g=c.substr(e-d+1);c=c.substr(0,e-d);this.index=f;break}if(this.isWhitespace(h))f++;else break}d={index:d,text:c};if(Ia.hasOwnProperty(c))d.fn= -Ia[c],d.json=Ia[c];else{var m=rc(c,this.options,this.text);d.fn=u(function(a,c){return m(a,c)},{assign:function(d,e){return ib(d,c,e,a.text,a.options)}})}this.tokens.push(d);g&&(this.tokens.push({index:e,text:".",json:!1}),this.tokens.push({index:e+1,text:g,json:!1}))},readString:function(a){var c=this.index;this.index++;for(var d="",e=a,f=!1;this.index<this.text.length;){var g=this.text.charAt(this.index),e=e+g;if(f)"u"===g?(g=this.text.substring(this.index+1,this.index+5),g.match(/[\da-f]{4}/i)|| -this.throwError("Invalid unicode escape [\\u"+g+"]"),this.index+=4,d+=String.fromCharCode(parseInt(g,16))):d=(f=Qd[g])?d+f:d+g,f=!1;else if("\\"===g)f=!0;else{if(g===a){this.index++;this.tokens.push({index:c,text:e,string:d,json:!0,fn:function(){return d}});return}d+=g}this.index++}this.throwError("Unterminated quote",c)}};var Ya=function(a,c,d){this.lexer=a;this.$filter=c;this.options=d};Ya.ZERO=function(){return 0};Ya.prototype={constructor:Ya,parse:function(a,c){this.text=a;this.json=c;this.tokens= -this.lexer.lex(a);c&&(this.assignment=this.logicalOR,this.functionCall=this.fieldAccess=this.objectIndex=this.filterChain=function(){this.throwError("is not valid json",{text:a,index:0})});var d=c?this.primary():this.statements();0!==this.tokens.length&&this.throwError("is an unexpected token",this.tokens[0]);d.literal=!!d.literal;d.constant=!!d.constant;return d},primary:function(){var a;if(this.expect("("))a=this.filterChain(),this.consume(")");else if(this.expect("["))a=this.arrayDeclaration(); -else if(this.expect("{"))a=this.object();else{var c=this.expect();(a=c.fn)||this.throwError("not a primary expression",c);c.json&&(a.constant=!0,a.literal=!0)}for(var d;c=this.expect("(","[",".");)"("===c.text?(a=this.functionCall(a,d),d=null):"["===c.text?(d=a,a=this.objectIndex(a)):"."===c.text?(d=a,a=this.fieldAccess(a)):this.throwError("IMPOSSIBLE");return a},throwError:function(a,c){throw pa("syntax",c.text,a,c.index+1,this.text,this.text.substring(c.index));},peekToken:function(){if(0===this.tokens.length)throw pa("ueoe", -this.text);return this.tokens[0]},peek:function(a,c,d,e){if(0<this.tokens.length){var f=this.tokens[0],g=f.text;if(g===a||g===c||g===d||g===e||!(a||c||d||e))return f}return!1},expect:function(a,c,d,e){return(a=this.peek(a,c,d,e))?(this.json&&!a.json&&this.throwError("is not valid json",a),this.tokens.shift(),a):!1},consume:function(a){this.expect(a)||this.throwError("is unexpected, expecting ["+a+"]",this.peek())},unaryFn:function(a,c){return u(function(d,e){return a(d,e,c)},{constant:c.constant})}, -ternaryFn:function(a,c,d){return u(function(e,f){return a(e,f)?c(e,f):d(e,f)},{constant:a.constant&&c.constant&&d.constant})},binaryFn:function(a,c,d){return u(function(e,f){return c(e,f,a,d)},{constant:a.constant&&d.constant})},statements:function(){for(var a=[];;)if(0<this.tokens.length&&!this.peek("}",")",";","]")&&a.push(this.filterChain()),!this.expect(";"))return 1===a.length?a[0]:function(c,d){for(var e,f=0;f<a.length;f++){var g=a[f];g&&(e=g(c,d))}return e}},filterChain:function(){for(var a= -this.expression(),c;;)if(c=this.expect("|"))a=this.binaryFn(a,c.fn,this.filter());else return a},filter:function(){for(var a=this.expect(),c=this.$filter(a.text),d=[];;)if(a=this.expect(":"))d.push(this.expression());else{var e=function(a,e,h){h=[h];for(var m=0;m<d.length;m++)h.push(d[m](a,e));return c.apply(a,h)};return function(){return e}}},expression:function(){return this.assignment()},assignment:function(){var a=this.ternary(),c,d;return(d=this.expect("="))?(a.assign||this.throwError("implies assignment but ["+ -this.text.substring(0,d.index)+"] can not be assigned to",d),c=this.ternary(),function(d,f){return a.assign(d,c(d,f),f)}):a},ternary:function(){var a=this.logicalOR(),c,d;if(this.expect("?")){c=this.ternary();if(d=this.expect(":"))return this.ternaryFn(a,c,this.ternary());this.throwError("expected :",d)}else return a},logicalOR:function(){for(var a=this.logicalAND(),c;;)if(c=this.expect("||"))a=this.binaryFn(a,c.fn,this.logicalAND());else return a},logicalAND:function(){var a=this.equality(),c;if(c= -this.expect("&&"))a=this.binaryFn(a,c.fn,this.logicalAND());return a},equality:function(){var a=this.relational(),c;if(c=this.expect("==","!=","===","!=="))a=this.binaryFn(a,c.fn,this.equality());return a},relational:function(){var a=this.additive(),c;if(c=this.expect("<",">","<=",">="))a=this.binaryFn(a,c.fn,this.relational());return a},additive:function(){for(var a=this.multiplicative(),c;c=this.expect("+","-");)a=this.binaryFn(a,c.fn,this.multiplicative());return a},multiplicative:function(){for(var a= -this.unary(),c;c=this.expect("*","/","%");)a=this.binaryFn(a,c.fn,this.unary());return a},unary:function(){var a;return this.expect("+")?this.primary():(a=this.expect("-"))?this.binaryFn(Ya.ZERO,a.fn,this.unary()):(a=this.expect("!"))?this.unaryFn(a.fn,this.unary()):this.primary()},fieldAccess:function(a){var c=this,d=this.expect().text,e=rc(d,this.options,this.text);return u(function(c,d,h){return e(h||a(c,d),d)},{assign:function(e,g,h){return ib(a(e,h),d,g,c.text,c.options)}})},objectIndex:function(a){var c= -this,d=this.expression();this.consume("]");return u(function(e,f){var g=a(e,f),h=ha(d(e,f),c.text,!0),m;if(!g)return s;(g=Xa(g[h],c.text))&&(g.then&&c.options.unwrapPromises)&&(m=g,"$$v"in g||(m.$$v=s,m.then(function(a){m.$$v=a})),g=g.$$v);return g},{assign:function(e,f,g){var h=ha(d(e,g),c.text);return Xa(a(e,g),c.text)[h]=f}})},functionCall:function(a,c){var d=[];if(")"!==this.peekToken().text){do d.push(this.expression());while(this.expect(","))}this.consume(")");var e=this;return function(f,g){for(var h= -[],m=c?c(f,g):f,k=0;k<d.length;k++)h.push(d[k](f,g));k=a(f,g,m)||t;Xa(m,e.text);Xa(k,e.text);h=k.apply?k.apply(m,h):k(h[0],h[1],h[2],h[3],h[4]);return Xa(h,e.text)}},arrayDeclaration:function(){var a=[],c=!0;if("]"!==this.peekToken().text){do{var d=this.expression();a.push(d);d.constant||(c=!1)}while(this.expect(","))}this.consume("]");return u(function(c,d){for(var g=[],h=0;h<a.length;h++)g.push(a[h](c,d));return g},{literal:!0,constant:c})},object:function(){var a=[],c=!0;if("}"!==this.peekToken().text){do{var d= -this.expect(),d=d.string||d.text;this.consume(":");var e=this.expression();a.push({key:d,value:e});e.constant||(c=!1)}while(this.expect(","))}this.consume("}");return u(function(c,d){for(var e={},m=0;m<a.length;m++){var k=a[m];e[k.key]=k.value(c,d)}return e},{literal:!0,constant:c})}};var Ib={},ra=L("$sce"),ca={HTML:"html",CSS:"css",URL:"url",RESOURCE_URL:"resourceUrl",JS:"js"},R=O.createElement("a"),uc=xa(W.location.href,!0);vc.$inject=["$provide"];wc.$inject=["$locale"];yc.$inject=["$locale"];var Bc= -".",Kd={yyyy:V("FullYear",4),yy:V("FullYear",2,0,!0),y:V("FullYear",1),MMMM:jb("Month"),MMM:jb("Month",!0),MM:V("Month",2,1),M:V("Month",1,1),dd:V("Date",2),d:V("Date",1),HH:V("Hours",2),H:V("Hours",1),hh:V("Hours",2,-12),h:V("Hours",1,-12),mm:V("Minutes",2),m:V("Minutes",1),ss:V("Seconds",2),s:V("Seconds",1),sss:V("Milliseconds",3),EEEE:jb("Day"),EEE:jb("Day",!0),a:function(a,c){return 12>a.getHours()?c.AMPMS[0]:c.AMPMS[1]},Z:function(a){a=-1*a.getTimezoneOffset();return a=(0<=a?"+":"")+(Kb(Math[0< -a?"floor":"ceil"](a/60),2)+Kb(Math.abs(a%60),2))}},Jd=/((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z))(.*)/,Id=/^\-?\d+$/;xc.$inject=["$locale"];var Gd=aa(w),Hd=aa(Ga);zc.$inject=["$parse"];var Rd=aa({restrict:"E",compile:function(a,c){8>=P&&(c.href||c.name||c.$set("href",""),a.append(O.createComment("IE fix")));return function(a,c){c.on("click",function(a){c.attr("href")||a.preventDefault()})}}}),Mb={};q(fb,function(a,c){if("multiple"!=a){var d=ka("ng-"+c);Mb[d]=function(){return{priority:100, -compile:function(){return function(a,f,g){a.$watch(g[d],function(a){g.$set(c,!!a)})}}}}}});q(["src","srcset","href"],function(a){var c=ka("ng-"+a);Mb[c]=function(){return{priority:99,link:function(d,e,f){f.$observe(c,function(c){c&&(f.$set(a,c),P&&e.prop(a,f[a]))})}}}});var mb={$addControl:t,$removeControl:t,$setValidity:t,$setDirty:t,$setPristine:t};Cc.$inject=["$element","$attrs","$scope"];var Ec=function(a){return["$timeout",function(c){return{name:"form",restrict:a?"EAC":"E",controller:Cc,compile:function(){return{pre:function(a, -e,f,g){if(!f.action){var h=function(a){a.preventDefault?a.preventDefault():a.returnValue=!1};Dc(e[0],"submit",h);e.on("$destroy",function(){c(function(){Ab(e[0],"submit",h)},0,!1)})}var m=e.parent().controller("form"),k=f.name||f.ngForm;k&&ib(a,k,g,k);if(m)e.on("$destroy",function(){m.$removeControl(g);k&&ib(a,k,s,k);u(g,mb)})}}}}}]},Sd=Ec(),Td=Ec(!0),Ud=/^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/,Vd=/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,6}$/,Wd= -/^\s*(\-|\+)?(\d+|(\d*(\.\d*)))\s*$/,Fc={text:ob,number:function(a,c,d,e,f,g){ob(a,c,d,e,f,g);e.$parsers.push(function(a){var c=e.$isEmpty(a);if(c||Wd.test(a))return e.$setValidity("number",!0),""===a?null:c?a:parseFloat(a);e.$setValidity("number",!1);return s});e.$formatters.push(function(a){return e.$isEmpty(a)?"":""+a});d.min&&(a=function(a){var c=parseFloat(d.min);if(!e.$isEmpty(a)&&a<c)return e.$setValidity("min",!1),s;e.$setValidity("min",!0);return a},e.$parsers.push(a),e.$formatters.push(a)); -d.max&&(a=function(a){var c=parseFloat(d.max);if(!e.$isEmpty(a)&&a>c)return e.$setValidity("max",!1),s;e.$setValidity("max",!0);return a},e.$parsers.push(a),e.$formatters.push(a));e.$formatters.push(function(a){if(e.$isEmpty(a)||qb(a))return e.$setValidity("number",!0),a;e.$setValidity("number",!1);return s})},url:function(a,c,d,e,f,g){ob(a,c,d,e,f,g);a=function(a){if(e.$isEmpty(a)||Ud.test(a))return e.$setValidity("url",!0),a;e.$setValidity("url",!1);return s};e.$formatters.push(a);e.$parsers.push(a)}, -email:function(a,c,d,e,f,g){ob(a,c,d,e,f,g);a=function(a){if(e.$isEmpty(a)||Vd.test(a))return e.$setValidity("email",!0),a;e.$setValidity("email",!1);return s};e.$formatters.push(a);e.$parsers.push(a)},radio:function(a,c,d,e){x(d.name)&&c.attr("name",Za());c.on("click",function(){c[0].checked&&a.$apply(function(){e.$setViewValue(d.value)})});e.$render=function(){c[0].checked=d.value==e.$viewValue};d.$observe("value",e.$render)},checkbox:function(a,c,d,e){var f=d.ngTrueValue,g=d.ngFalseValue;D(f)|| -(f=!0);D(g)||(g=!1);c.on("click",function(){a.$apply(function(){e.$setViewValue(c[0].checked)})});e.$render=function(){c[0].checked=e.$viewValue};e.$isEmpty=function(a){return a!==f};e.$formatters.push(function(a){return a===f});e.$parsers.push(function(a){return a?f:g})},hidden:t,button:t,submit:t,reset:t},Gc=["$browser","$sniffer",function(a,c){return{restrict:"E",require:"?ngModel",link:function(d,e,f,g){g&&(Fc[w(f.type)]||Fc.text)(d,e,f,g,c,a)}}}],lb="ng-valid",kb="ng-invalid",Ha="ng-pristine", -nb="ng-dirty",Xd=["$scope","$exceptionHandler","$attrs","$element","$parse",function(a,c,d,e,f){function g(a,c){c=c?"-"+cb(c,"-"):"";e.removeClass((a?kb:lb)+c).addClass((a?lb:kb)+c)}this.$modelValue=this.$viewValue=Number.NaN;this.$parsers=[];this.$formatters=[];this.$viewChangeListeners=[];this.$pristine=!0;this.$dirty=!1;this.$valid=!0;this.$invalid=!1;this.$name=d.name;var h=f(d.ngModel),m=h.assign;if(!m)throw L("ngModel")("nonassign",d.ngModel,ea(e));this.$render=t;this.$isEmpty=function(a){return x(a)|| -""===a||null===a||a!==a};var k=e.inheritedData("$formController")||mb,l=0,n=this.$error={};e.addClass(Ha);g(!0);this.$setValidity=function(a,c){n[a]!==!c&&(c?(n[a]&&l--,l||(g(!0),this.$valid=!0,this.$invalid=!1)):(g(!1),this.$invalid=!0,this.$valid=!1,l++),n[a]=!c,g(c,a),k.$setValidity(a,c,this))};this.$setPristine=function(){this.$dirty=!1;this.$pristine=!0;e.removeClass(nb).addClass(Ha)};this.$setViewValue=function(d){this.$viewValue=d;this.$pristine&&(this.$dirty=!0,this.$pristine=!1,e.removeClass(Ha).addClass(nb), -k.$setDirty());q(this.$parsers,function(a){d=a(d)});this.$modelValue!==d&&(this.$modelValue=d,m(a,d),q(this.$viewChangeListeners,function(a){try{a()}catch(d){c(d)}}))};var r=this;a.$watch(function(){var c=h(a);if(r.$modelValue!==c){var d=r.$formatters,e=d.length;for(r.$modelValue=c;e--;)c=d[e](c);r.$viewValue!==c&&(r.$viewValue=c,r.$render())}})}],Yd=function(){return{require:["ngModel","^?form"],controller:Xd,link:function(a,c,d,e){var f=e[0],g=e[1]||mb;g.$addControl(f);a.$on("$destroy",function(){g.$removeControl(f)})}}}, -Zd=aa({require:"ngModel",link:function(a,c,d,e){e.$viewChangeListeners.push(function(){a.$eval(d.ngChange)})}}),Hc=function(){return{require:"?ngModel",link:function(a,c,d,e){if(e){d.required=!0;var f=function(a){if(d.required&&e.$isEmpty(a))e.$setValidity("required",!1);else return e.$setValidity("required",!0),a};e.$formatters.push(f);e.$parsers.unshift(f);d.$observe("required",function(){f(e.$viewValue)})}}}},$d=function(){return{require:"ngModel",link:function(a,c,d,e){var f=(a=/\/(.*)\//.exec(d.ngList))&& -RegExp(a[1])||d.ngList||",";e.$parsers.push(function(a){if(!x(a)){var c=[];a&&q(a.split(f),function(a){a&&c.push(Y(a))});return c}});e.$formatters.push(function(a){return J(a)?a.join(", "):s});e.$isEmpty=function(a){return!a||!a.length}}}},ae=/^(true|false|\d+)$/,be=function(){return{priority:100,compile:function(a,c){return ae.test(c.ngValue)?function(a,c,f){f.$set("value",a.$eval(f.ngValue))}:function(a,c,f){a.$watch(f.ngValue,function(a){f.$set("value",a)})}}}},ce=sa(function(a,c,d){c.addClass("ng-binding").data("$binding", -d.ngBind);a.$watch(d.ngBind,function(a){c.text(a==s?"":a)})}),de=["$interpolate",function(a){return function(c,d,e){c=a(d.attr(e.$attr.ngBindTemplate));d.addClass("ng-binding").data("$binding",c);e.$observe("ngBindTemplate",function(a){d.text(a)})}}],ee=["$sce","$parse",function(a,c){return function(d,e,f){e.addClass("ng-binding").data("$binding",f.ngBindHtml);var g=c(f.ngBindHtml);d.$watch(function(){return(g(d)||"").toString()},function(c){e.html(a.getTrustedHtml(g(d))||"")})}}],fe=Lb("",!0),ge= -Lb("Odd",0),he=Lb("Even",1),ie=sa({compile:function(a,c){c.$set("ngCloak",s);a.removeClass("ng-cloak")}}),je=[function(){return{scope:!0,controller:"@"}}],Ic={};q("click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste".split(" "),function(a){var c=ka("ng-"+a);Ic[c]=["$parse",function(d){return{compile:function(e,f){var g=d(f[c]);return function(c,d,e){d.on(w(a),function(a){c.$apply(function(){g(c,{$event:a})})})}}}}]}); -var ke=["$animate",function(a){return{transclude:"element",priority:600,terminal:!0,restrict:"A",$$tlb:!0,compile:function(c,d,e){return function(c,d,h){var m,k;c.$watch(h.ngIf,function(l){Na(l)?(k=c.$new(),e(k,function(c){m={startNode:c[0],endNode:c[c.length++]=O.createComment(" end ngIf: "+h.ngIf+" ")};a.enter(c,d.parent(),d)})):(k&&(k.$destroy(),k=null),m&&(a.leave(vb(m)),m=null))})}}}}],le=["$http","$templateCache","$anchorScroll","$compile","$animate","$sce",function(a,c,d,e,f,g){return{restrict:"ECA", -priority:400,terminal:!0,transclude:"element",compile:function(h,m,k){var l=m.ngInclude||m.src,n=m.onload||"",r=m.autoscroll;return function(h,m){var q=0,s,t,y=function(){s&&(s.$destroy(),s=null);t&&(f.leave(t),t=null)};h.$watch(g.parseAsResourceUrl(l),function(g){var l=function(){!z(r)||r&&!h.$eval(r)||d()},x=++q;g?(a.get(g,{cache:c}).success(function(a){if(x===q){var c=h.$new();k(c,function(d){y();s=c;t=d;t.html(a);f.enter(t,null,m,l);e(t.contents())(s);s.$emit("$includeContentLoaded");h.$eval(n)})}}).error(function(){x=== -q&&y()}),h.$emit("$includeContentRequested")):y()})}}}}],me=sa({compile:function(){return{pre:function(a,c,d){a.$eval(d.ngInit)}}}}),ne=sa({terminal:!0,priority:1E3}),oe=["$locale","$interpolate",function(a,c){var d=/{}/g;return{restrict:"EA",link:function(e,f,g){var h=g.count,m=g.$attr.when&&f.attr(g.$attr.when),k=g.offset||0,l=e.$eval(m)||{},n={},r=c.startSymbol(),p=c.endSymbol(),s=/^when(Minus)?(.+)$/;q(g,function(a,c){s.test(c)&&(l[w(c.replace("when","").replace("Minus","-"))]=f.attr(g.$attr[c]))}); -q(l,function(a,e){n[e]=c(a.replace(d,r+h+"-"+k+p))});e.$watch(function(){var c=parseFloat(e.$eval(h));if(isNaN(c))return"";c in l||(c=a.pluralCat(c-k));return n[c](e,f,!0)},function(a){f.text(a)})}}}],pe=["$parse","$animate",function(a,c){var d=L("ngRepeat");return{transclude:"element",priority:1E3,terminal:!0,$$tlb:!0,compile:function(e,f,g){return function(e,f,k){var l=k.ngRepeat,n=l.match(/^\s*(.+)\s+in\s+(.*?)\s*(\s+track\s+by\s+(.+)\s*)?$/),r,p,s,t,z,A,x,G={$id:Ca};if(!n)throw d("iexp",l);k= -n[1];z=n[2];(n=n[4])?(r=a(n),p=function(a,c,d){x&&(G[x]=a);G[A]=c;G.$index=d;return r(e,G)}):(s=function(a,c){return Ca(c)},t=function(a){return a});n=k.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/);if(!n)throw d("iidexp",k);A=n[3]||n[1];x=n[2];var v={};e.$watchCollection(z,function(a){var k,n,r=f[0],z,M={},G,N,w,I,D,u,J=[];if(pb(a))D=a,z=p||s;else{z=p||t;D=[];for(w in a)a.hasOwnProperty(w)&&"$"!=w.charAt(0)&&D.push(w);D.sort()}G=D.length;n=J.length=D.length;for(k=0;k<n;k++)if(w=a===D?k: -D[k],I=a[w],I=z(w,I,k),na(I,"`track by` id"),v.hasOwnProperty(I))u=v[I],delete v[I],M[I]=u,J[k]=u;else{if(M.hasOwnProperty(I))throw q(J,function(a){a&&a.startNode&&(v[a.id]=a)}),d("dupes",l,I);J[k]={id:I};M[I]=!1}for(w in v)v.hasOwnProperty(w)&&(u=v[w],k=vb(u),c.leave(k),q(k,function(a){a.$$NG_REMOVED=!0}),u.scope.$destroy());k=0;for(n=D.length;k<n;k++){w=a===D?k:D[k];I=a[w];u=J[k];J[k-1]&&(r=J[k-1].endNode);if(u.startNode){N=u.scope;z=r;do z=z.nextSibling;while(z&&z.$$NG_REMOVED);u.startNode!=z&& -c.move(vb(u),null,y(r));r=u.endNode}else N=e.$new();N[A]=I;x&&(N[x]=w);N.$index=k;N.$first=0===k;N.$last=k===G-1;N.$middle=!(N.$first||N.$last);N.$odd=!(N.$even=0===(k&1));u.startNode||g(N,function(a){a[a.length++]=O.createComment(" end ngRepeat: "+l+" ");c.enter(a,null,y(r));r=a;u.scope=N;u.startNode=r&&r.endNode?r.endNode:a[0];u.endNode=a[a.length-1];M[u.id]=u})}v=M})}}}}],qe=["$animate",function(a){return function(c,d,e){c.$watch(e.ngShow,function(c){a[Na(c)?"removeClass":"addClass"](d,"ng-hide")})}}], -re=["$animate",function(a){return function(c,d,e){c.$watch(e.ngHide,function(c){a[Na(c)?"addClass":"removeClass"](d,"ng-hide")})}}],se=sa(function(a,c,d){a.$watch(d.ngStyle,function(a,d){d&&a!==d&&q(d,function(a,d){c.css(d,"")});a&&c.css(a)},!0)}),te=["$animate",function(a){return{restrict:"EA",require:"ngSwitch",controller:["$scope",function(){this.cases={}}],link:function(c,d,e,f){var g,h,m=[];c.$watch(e.ngSwitch||e.on,function(d){for(var l=0,n=m.length;l<n;l++)m[l].$destroy(),a.leave(h[l]);h=[]; -m=[];if(g=f.cases["!"+d]||f.cases["?"])c.$eval(e.change),q(g,function(d){var e=c.$new();m.push(e);d.transclude(e,function(c){var e=d.element;h.push(c);a.enter(c,e.parent(),e)})})})}}}],ue=sa({transclude:"element",priority:800,require:"^ngSwitch",compile:function(a,c,d){return function(a,f,g,h){h.cases["!"+c.ngSwitchWhen]=h.cases["!"+c.ngSwitchWhen]||[];h.cases["!"+c.ngSwitchWhen].push({transclude:d,element:f})}}}),ve=sa({transclude:"element",priority:800,require:"^ngSwitch",compile:function(a,c,d){return function(a, -c,g,h){h.cases["?"]=h.cases["?"]||[];h.cases["?"].push({transclude:d,element:c})}}}),we=sa({controller:["$element","$transclude",function(a,c){if(!c)throw L("ngTransclude")("orphan",ea(a));this.$transclude=c}],link:function(a,c,d,e){e.$transclude(function(a){c.html("");c.append(a)})}}),xe=["$templateCache",function(a){return{restrict:"E",terminal:!0,compile:function(c,d){"text/ng-template"==d.type&&a.put(d.id,c[0].text)}}}],ye=L("ngOptions"),ze=aa({terminal:!0}),Ae=["$compile","$parse",function(a, -c){var d=/^\s*(.*?)(?:\s+as\s+(.*?))?(?:\s+group\s+by\s+(.*))?\s+for\s+(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+(.*?)(?:\s+track\s+by\s+(.*?))?$/,e={$setViewValue:t};return{restrict:"E",require:["select","?ngModel"],controller:["$element","$scope","$attrs",function(a,c,d){var m=this,k={},l=e,n;m.databound=d.ngModel;m.init=function(a,c,d){l=a;n=d};m.addOption=function(c){na(c,'"option value"');k[c]=!0;l.$viewValue==c&&(a.val(c),n.parent()&&n.remove())};m.removeOption= -function(a){this.hasOption(a)&&(delete k[a],l.$viewValue==a&&this.renderUnknownOption(a))};m.renderUnknownOption=function(c){c="? "+Ca(c)+" ?";n.val(c);a.prepend(n);a.val(c);n.prop("selected",!0)};m.hasOption=function(a){return k.hasOwnProperty(a)};c.$on("$destroy",function(){m.renderUnknownOption=t})}],link:function(e,g,h,m){function k(a,c,d,e){d.$render=function(){var a=d.$viewValue;e.hasOption(a)?(v.parent()&&v.remove(),c.val(a),""===a&&A.prop("selected",!0)):x(a)&&A?c.val(""):e.renderUnknownOption(a)}; -c.on("change",function(){a.$apply(function(){v.parent()&&v.remove();d.$setViewValue(c.val())})})}function l(a,c,d){var e;d.$render=function(){var a=new Sa(d.$viewValue);q(c.find("option"),function(c){c.selected=z(a.get(c.value))})};a.$watch(function(){Aa(e,d.$viewValue)||(e=da(d.$viewValue),d.$render())});c.on("change",function(){a.$apply(function(){var a=[];q(c.find("option"),function(c){c.selected&&a.push(c.value)});d.$setViewValue(a)})})}function n(e,f,g){function h(){var a={"":[]},c=[""],d,k, -s,u,x;u=g.$modelValue;x=r(e)||[];var A=n?Nb(x):x,H,K,B;K={};s=!1;var E,L;if(t)if(v&&J(u))for(s=new Sa([]),B=0;B<u.length;B++)K[m]=u[B],s.put(v(e,K),u[B]);else s=new Sa(u);for(B=0;H=A.length,B<H;B++){k=B;if(n){k=A[B];if("$"===k.charAt(0))continue;K[n]=k}K[m]=x[k];d=p(e,K)||"";(k=a[d])||(k=a[d]=[],c.push(d));t?d=z(s.remove(v?v(e,K):q(e,K))):(v?(d={},d[m]=u,d=v(e,d)===v(e,K)):d=u===q(e,K),s=s||d);E=l(e,K);E=z(E)?E:"";k.push({id:v?v(e,K):n?A[B]:B,label:E,selected:d})}t||(w||null===u?a[""].unshift({id:"", -label:"",selected:!s}):s||a[""].unshift({id:"?",label:"",selected:!0}));K=0;for(A=c.length;K<A;K++){d=c[K];k=a[d];y.length<=K?(u={element:G.clone().attr("label",d),label:k.label},x=[u],y.push(x),f.append(u.element)):(x=y[K],u=x[0],u.label!=d&&u.element.attr("label",u.label=d));E=null;B=0;for(H=k.length;B<H;B++)s=k[B],(d=x[B+1])?(E=d.element,d.label!==s.label&&E.text(d.label=s.label),d.id!==s.id&&E.val(d.id=s.id),E[0].selected!==s.selected&&E.prop("selected",d.selected=s.selected)):(""===s.id&&w?L= -w:(L=D.clone()).val(s.id).attr("selected",s.selected).text(s.label),x.push({element:L,label:s.label,id:s.id,selected:s.selected}),E?E.after(L):u.element.append(L),E=L);for(B++;x.length>B;)x.pop().element.remove()}for(;y.length>K;)y.pop()[0].element.remove()}var k;if(!(k=u.match(d)))throw ye("iexp",u,ea(f));var l=c(k[2]||k[1]),m=k[4]||k[6],n=k[5],p=c(k[3]||""),q=c(k[2]?k[1]:m),r=c(k[7]),v=k[8]?c(k[8]):null,y=[[{element:f,label:""}]];w&&(a(w)(e),w.removeClass("ng-scope"),w.remove());f.html("");f.on("change", -function(){e.$apply(function(){var a,c=r(e)||[],d={},h,k,l,p,u,x,w;if(t)for(k=[],p=0,x=y.length;p<x;p++)for(a=y[p],l=1,u=a.length;l<u;l++){if((h=a[l].element)[0].selected){h=h.val();n&&(d[n]=h);if(v)for(w=0;w<c.length&&(d[m]=c[w],v(e,d)!=h);w++);else d[m]=c[h];k.push(q(e,d))}}else if(h=f.val(),"?"==h)k=s;else if(""===h)k=null;else if(v)for(w=0;w<c.length;w++){if(d[m]=c[w],v(e,d)==h){k=q(e,d);break}}else d[m]=c[h],n&&(d[n]=h),k=q(e,d);g.$setViewValue(k)})});g.$render=h;e.$watch(h)}if(m[1]){var r=m[0], -p=m[1],t=h.multiple,u=h.ngOptions,w=!1,A,D=y(O.createElement("option")),G=y(O.createElement("optgroup")),v=D.clone();m=0;for(var B=g.children(),E=B.length;m<E;m++)if(""===B[m].value){A=w=B.eq(m);break}r.init(p,w,v);if(t&&(h.required||h.ngRequired)){var L=function(a){p.$setValidity("required",!h.required||a&&a.length);return a};p.$parsers.push(L);p.$formatters.unshift(L);h.$observe("required",function(){L(p.$viewValue)})}u?n(e,g,p):t?l(e,g,p):k(e,g,p,r)}}}}],Be=["$interpolate",function(a){var c={addOption:t, -removeOption:t};return{restrict:"E",priority:100,compile:function(d,e){if(x(e.value)){var f=a(d.text(),!0);f||e.$set("value",d.text())}return function(a,d,e){var k=d.parent(),l=k.data("$selectController")||k.parent().data("$selectController");l&&l.databound?d.prop("selected",!1):l=c;f?a.$watch(f,function(a,c){e.$set("value",a);a!==c&&l.removeOption(c);l.addOption(a)}):l.addOption(e.value);d.on("$destroy",function(){l.removeOption(e.value)})}}}}],Ce=aa({restrict:"E",terminal:!0});(Ba=W.jQuery)?(y= -Ba,u(Ba.fn,{scope:Da.scope,isolateScope:Da.isolateScope,controller:Da.controller,injector:Da.injector,inheritedData:Da.inheritedData}),wb("remove",!0,!0,!1),wb("empty",!1,!1,!1),wb("html",!1,!1,!0)):y=Q;bb.element=y;(function(a){u(a,{bootstrap:Wb,copy:da,extend:u,equals:Aa,element:y,forEach:q,injector:Xb,noop:t,bind:rb,toJson:ma,fromJson:Sb,identity:za,isUndefined:x,isDefined:z,isString:D,isFunction:B,isObject:T,isNumber:qb,isElement:Kc,isArray:J,version:Md,isDate:Ja,lowercase:w,uppercase:Ga,callbacks:{counter:0}, -$$minErr:L,$$csp:Rb});Ua=Qc(W);try{Ua("ngLocale")}catch(c){Ua("ngLocale",[]).provider("$locale",pd)}Ua("ng",["ngLocale"],["$provide",function(a){a.provider("$compile",ec).directive({a:Rd,input:Gc,textarea:Gc,form:Sd,script:xe,select:Ae,style:Ce,option:Be,ngBind:ce,ngBindHtml:ee,ngBindTemplate:de,ngClass:fe,ngClassEven:he,ngClassOdd:ge,ngCloak:ie,ngController:je,ngForm:Td,ngHide:re,ngIf:ke,ngInclude:le,ngInit:me,ngNonBindable:ne,ngPluralize:oe,ngRepeat:pe,ngShow:qe,ngStyle:se,ngSwitch:te,ngSwitchWhen:ue, -ngSwitchDefault:ve,ngOptions:ze,ngTransclude:we,ngModel:Yd,ngList:$d,ngChange:Zd,required:Hc,ngRequired:Hc,ngValue:be}).directive(Mb).directive(Ic);a.provider({$anchorScroll:Zc,$animate:Od,$browser:bd,$cacheFactory:cd,$controller:gd,$document:hd,$exceptionHandler:id,$filter:vc,$interpolate:nd,$interval:od,$http:jd,$httpBackend:kd,$location:rd,$log:sd,$parse:td,$rootScope:wd,$q:ud,$sce:zd,$sceDelegate:yd,$sniffer:Ad,$templateCache:dd,$timeout:Bd,$window:Cd})}])})(bb);y(O).ready(function(){Oc(O,Wb)})})(window, -document);!angular.$$csp()&&angular.element(document).find("head").prepend('<style type="text/css">@charset "UTF-8";[ng\\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak,.ng-hide{display:none !important;}ng\\:form{display:block;}.ng-animate-start{clip:rect(0,auto,auto,0);-ms-zoom:1.0001;}.ng-animate-active{clip:rect(-1px,auto,auto,0);-ms-zoom:1;}</style>'); +(function(t,Y,s){'use strict';function K(b){return function(){var a=arguments[0],c;c="["+(b?b+":":"")+a+"] http://errors.angularjs.org/1.3.0-rc.1/"+(b?b+"/":"")+a;for(a=1;a<arguments.length;a++){c=c+(1==a?"?":"&")+"p"+(a-1)+"=";var d=encodeURIComponent,e;e=arguments[a];e="function"==typeof e?e.toString().replace(/ \{[\s\S]*$/,""):"undefined"==typeof e?"undefined":"string"!=typeof e?JSON.stringify(e):e;c+=d(e)}return Error(c)}}function Na(b){if(null==b||Oa(b))return!1;var a=b.length;return 1===b.nodeType&& +a?!0:C(b)||L(b)||0===a||"number"===typeof a&&0<a&&a-1 in b}function r(b,a,c){var d,e;if(b)if(D(b))for(d in b)"prototype"==d||"length"==d||"name"==d||b.hasOwnProperty&&!b.hasOwnProperty(d)||a.call(c,b[d],d,b);else if(L(b)||Na(b)){var f="object"!==typeof b;d=0;for(e=b.length;d<e;d++)(f||d in b)&&a.call(c,b[d],d,b)}else if(b.forEach&&b.forEach!==r)b.forEach(a,c,b);else for(d in b)b.hasOwnProperty(d)&&a.call(c,b[d],d,b);return b}function Zb(b){var a=[],c;for(c in b)b.hasOwnProperty(c)&&a.push(c);return a.sort()} +function od(b,a,c){for(var d=Zb(b),e=0;e<d.length;e++)a.call(c,b[d[e]],d[e]);return d}function $b(b){return function(a,c){b(c,a)}}function pd(){return++bb}function ac(b,a){a?b.$$hashKey=a:delete b.$$hashKey}function E(b){for(var a=b.$$hashKey,c=1,d=arguments.length;c<d;c++){var e=arguments[c];if(e)for(var f=Object.keys(e),g=0,h=f.length;g<h;g++){var m=f[g];b[m]=e[m]}}ac(b,a);return b}function U(b){return parseInt(b,10)}function bc(b,a){return E(new (E(function(){},{prototype:b})),a)}function w(){} +function Pa(b){return b}function da(b){return function(){return b}}function F(b){return"undefined"===typeof b}function B(b){return"undefined"!==typeof b}function S(b){return null!==b&&"object"===typeof b}function C(b){return"string"===typeof b}function ea(b){return"number"===typeof b}function fa(b){return"[object Date]"===Ga.call(b)}function D(b){return"function"===typeof b}function cb(b){return"[object RegExp]"===Ga.call(b)}function Oa(b){return b&&b.window===b}function Qa(b){return b&&b.$evalAsync&& +b.$watch}function qd(b){return!(!b||!(b.nodeName||b.prop&&b.attr&&b.find))}function rd(b){var a={};b=b.split(",");var c;for(c=0;c<b.length;c++)a[b[c]]=!0;return a}function pa(b){return P(b.nodeName||b[0].nodeName)}function sd(b,a,c){var d=[];r(b,function(b,f,g){d.push(a.call(c,b,f,g))});return d}function Ra(b,a){var c=b.indexOf(a);0<=c&&b.splice(c,1);return a}function Ha(b,a,c,d){if(Oa(b)||Qa(b))throw Sa("cpws");if(a){if(b===a)throw Sa("cpi");c=c||[];d=d||[];if(S(b)){var e=c.indexOf(b);if(-1!==e)return d[e]; +c.push(b);d.push(a)}if(L(b))for(var f=a.length=0;f<b.length;f++)e=Ha(b[f],null,c,d),S(b[f])&&(c.push(b[f]),d.push(e)),a.push(e);else{var g=a.$$hashKey;L(a)?a.length=0:r(a,function(c,b){delete a[b]});for(f in b)b.hasOwnProperty(f)&&(e=Ha(b[f],null,c,d),S(b[f])&&(c.push(b[f]),d.push(e)),a[f]=e);ac(a,g)}}else if(a=b)L(b)?a=Ha(b,[],c,d):fa(b)?a=new Date(b.getTime()):cb(b)?(a=new RegExp(b.source,b.toString().match(/[^\/]*$/)[0]),a.lastIndex=b.lastIndex):S(b)&&(e=Object.create(Object.getPrototypeOf(b)), +a=Ha(b,e,c,d));return a}function qa(b,a){if(L(b)){a=a||[];for(var c=0,d=b.length;c<d;c++)a[c]=b[c]}else if(S(b))for(c in a=a||{},b)if("$"!==c.charAt(0)||"$"!==c.charAt(1))a[c]=b[c];return a||b}function ra(b,a){if(b===a)return!0;if(null===b||null===a)return!1;if(b!==b&&a!==a)return!0;var c=typeof b,d;if(c==typeof a&&"object"==c)if(L(b)){if(!L(a))return!1;if((c=b.length)==a.length){for(d=0;d<c;d++)if(!ra(b[d],a[d]))return!1;return!0}}else{if(fa(b))return fa(a)?ra(b.getTime(),a.getTime()):!1;if(cb(b)&& +cb(a))return b.toString()==a.toString();if(Qa(b)||Qa(a)||Oa(b)||Oa(a)||L(a))return!1;c={};for(d in b)if("$"!==d.charAt(0)&&!D(b[d])){if(!ra(b[d],a[d]))return!1;c[d]=!0}for(d in a)if(!c.hasOwnProperty(d)&&"$"!==d.charAt(0)&&a[d]!==s&&!D(a[d]))return!1;return!0}return!1}function db(b,a,c){return b.concat(Ta.call(a,c))}function cc(b,a){var c=2<arguments.length?Ta.call(arguments,2):[];return!D(a)||a instanceof RegExp?a:c.length?function(){return arguments.length?a.apply(b,c.concat(Ta.call(arguments,0))): +a.apply(b,c)}:function(){return arguments.length?a.apply(b,arguments):a.call(b)}}function td(b,a){var c=a;"string"===typeof b&&"$"===b.charAt(0)&&"$"===b.charAt(1)?c=s:Oa(a)?c="$WINDOW":a&&Y===a?c="$DOCUMENT":Qa(a)&&(c="$SCOPE");return c}function sa(b,a){return"undefined"===typeof b?s:JSON.stringify(b,td,a?" ":null)}function dc(b){return C(b)?JSON.parse(b):b}function ta(b){b=G(b).clone();try{b.empty()}catch(a){}var c=G("<div>").append(b).html();try{return 3===b[0].nodeType?P(c):c.match(/^(<[^>]+>)/)[1].replace(/^<([\w\-]+)/, +function(a,c){return"<"+P(c)})}catch(d){return P(c)}}function ec(b){try{return decodeURIComponent(b)}catch(a){}}function fc(b){var a={},c,d;r((b||"").split("&"),function(b){b&&(c=b.replace(/\+/g,"%20").split("="),d=ec(c[0]),B(d)&&(b=B(c[1])?ec(c[1]):!0,Ab.call(a,d)?L(a[d])?a[d].push(b):a[d]=[a[d],b]:a[d]=b))});return a}function Bb(b){var a=[];r(b,function(b,d){L(b)?r(b,function(b){a.push(Da(d,!0)+(!0===b?"":"="+Da(b,!0)))}):a.push(Da(d,!0)+(!0===b?"":"="+Da(b,!0)))});return a.length?a.join("&"):""} +function eb(b){return Da(b,!0).replace(/%26/gi,"&").replace(/%3D/gi,"=").replace(/%2B/gi,"+")}function Da(b,a){return encodeURIComponent(b).replace(/%40/gi,"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%3B/gi,";").replace(/%20/g,a?"%20":"+")}function ud(b,a){var c,d,e=fb.length;b=G(b);for(d=0;d<e;++d)if(c=fb[d]+a,C(c=b.attr(c)))return c;return null}function vd(b,a){var c,d,e={};r(fb,function(a){a+="app";!c&&b.hasAttribute&&b.hasAttribute(a)&&(c=b,d=b.getAttribute(a))}); +r(fb,function(a){a+="app";var e;!c&&(e=b.querySelector("["+a.replace(":","\\:")+"]"))&&(c=e,d=e.getAttribute(a))});c&&(e.strictDi=null!==ud(c,"strict-di"),a(c,d?[d]:[],e))}function gc(b,a,c){S(c)||(c={});c=E({strictDi:!1},c);var d=function(){b=G(b);if(b.injector()){var d=b[0]===Y?"document":ta(b);throw Sa("btstrpd",d.replace(/</,"<").replace(/>/,">"));}a=a||[];a.unshift(["$provide",function(a){a.value("$rootElement",b)}]);c.debugInfoEnabled&&a.push(["$compileProvider",function(a){a.debugInfoEnabled(!0)}]); +a.unshift("ng");d=Cb(a,c.strictDi);d.invoke(["$rootScope","$rootElement","$compile","$injector",function(a,b,c,d){a.$apply(function(){b.data("$injector",d);c(b)(a)})}]);return d},e=/^NG_ENABLE_DEBUG_INFO!/,f=/^NG_DEFER_BOOTSTRAP!/;t&&e.test(t.name)&&(c.debugInfoEnabled=!0,t.name=t.name.replace(e,""));if(t&&!f.test(t.name))return d();t.name=t.name.replace(f,"");Ea.resumeBootstrap=function(b){r(b,function(b){a.push(b)});d()}}function wd(){t.name="NG_ENABLE_DEBUG_INFO!"+t.name;t.location.reload()}function xd(b){return Ea.element(b).injector().get("$$testability")} +function Db(b,a){a=a||"_";return b.replace(yd,function(b,d){return(d?a:"")+b.toLowerCase()})}function zd(){var b;hc||((la=t.jQuery)&&la.fn.on?(G=la,E(la.fn,{scope:Ia.scope,isolateScope:Ia.isolateScope,controller:Ia.controller,injector:Ia.injector,inheritedData:Ia.inheritedData}),b=la.cleanData,la.cleanData=function(a){var c;if(Eb)Eb=!1;else for(var d=0,e;null!=(e=a[d]);d++)(c=la._data(e,"events"))&&c.$destroy&&la(e).triggerHandler("$destroy");b(a)}):G=V,Ea.element=G,hc=!0)}function Fb(b,a,c){if(!b)throw Sa("areq", +a||"?",c||"required");return b}function gb(b,a,c){c&&L(b)&&(b=b[b.length-1]);Fb(D(b),a,"not a function, got "+(b&&"object"===typeof b?b.constructor.name||"Object":typeof b));return b}function Ja(b,a){if("hasOwnProperty"===b)throw Sa("badname",a);}function ic(b,a,c){if(!a)return b;a=a.split(".");for(var d,e=b,f=a.length,g=0;g<f;g++)d=a[g],b&&(b=(e=b)[d]);return!c&&D(b)?cc(e,b):b}function hb(b){var a=b[0];b=b[b.length-1];var c=[a];do{a=a.nextSibling;if(!a)break;c.push(a)}while(a!==b);return G(c)}function Ad(b){function a(a, +b,c){return a[b]||(a[b]=c())}var c=K("$injector"),d=K("ng");b=a(b,"angular",Object);b.$$minErr=b.$$minErr||K;return a(b,"module",function(){var b={};return function(f,g,h){if("hasOwnProperty"===f)throw d("badname","module");g&&b.hasOwnProperty(f)&&(b[f]=null);return a(b,f,function(){function a(c,d,e,f){f||(f=b);return function(){f[e||"push"]([c,d,arguments]);return q}}if(!g)throw c("nomod",f);var b=[],d=[],e=[],l=a("$injector","invoke","push",d),q={_invokeQueue:b,_configBlocks:d,_runBlocks:e,requires:g, +name:f,provider:a("$provide","provider"),factory:a("$provide","factory"),service:a("$provide","service"),value:a("$provide","value"),constant:a("$provide","constant","unshift"),animation:a("$animateProvider","register"),filter:a("$filterProvider","register"),controller:a("$controllerProvider","register"),directive:a("$compileProvider","directive"),config:l,run:function(a){e.push(a);return this}};h&&l(h);return q})}})}function Bd(b){E(b,{bootstrap:gc,copy:Ha,extend:E,equals:ra,element:G,forEach:r, +injector:Cb,noop:w,bind:cc,toJson:sa,fromJson:dc,identity:Pa,isUndefined:F,isDefined:B,isString:C,isFunction:D,isObject:S,isNumber:ea,isElement:qd,isArray:L,version:Cd,isDate:fa,lowercase:P,uppercase:ib,callbacks:{counter:0},getTestability:xd,$$minErr:K,$$csp:Ua,reloadWithDebugInfo:wd,$$hasClass:jb});Va=Ad(t);try{Va("ngLocale")}catch(a){Va("ngLocale",[]).provider("$locale",Dd)}Va("ng",["ngLocale"],["$provide",function(a){a.provider({$$sanitizeUri:Ed});a.provider("$compile",jc).directive({a:Fd,input:kc, +textarea:kc,form:Gd,script:Hd,select:Id,style:Jd,option:Kd,ngBind:Ld,ngBindHtml:Md,ngBindTemplate:Nd,ngClass:Od,ngClassEven:Pd,ngClassOdd:Qd,ngCloak:Rd,ngController:Sd,ngForm:Td,ngHide:Ud,ngIf:Vd,ngInclude:Wd,ngInit:Xd,ngNonBindable:Yd,ngPluralize:Zd,ngRepeat:$d,ngShow:ae,ngStyle:be,ngSwitch:ce,ngSwitchWhen:de,ngSwitchDefault:ee,ngOptions:fe,ngTransclude:ge,ngModel:he,ngList:ie,ngChange:je,pattern:lc,ngPattern:lc,required:mc,ngRequired:mc,minlength:nc,ngMinlength:nc,maxlength:oc,ngMaxlength:oc,ngValue:ke, +ngModelOptions:le}).directive({ngInclude:me}).directive(kb).directive(pc);a.provider({$anchorScroll:ne,$animate:oe,$browser:pe,$cacheFactory:qe,$controller:re,$document:se,$exceptionHandler:te,$filter:qc,$interpolate:ue,$interval:ve,$http:we,$httpBackend:xe,$location:ye,$log:ze,$parse:Ae,$rootScope:Be,$q:Ce,$$q:De,$sce:Ee,$sceDelegate:Fe,$sniffer:Ge,$templateCache:He,$templateRequest:Ie,$$testability:Je,$timeout:Ke,$window:Le,$$rAF:Me,$$asyncCallback:Ne})}])}function Wa(b){return b.replace(Oe,function(a, +b,d,e){return e?d.toUpperCase():d}).replace(Pe,"Moz$1")}function rc(b){b=b.nodeType;return 1===b||!b||9===b}function sc(b,a){var c,d,e=a.createDocumentFragment(),f=[];if(Gb.test(b)){c=c||e.appendChild(a.createElement("div"));d=(Qe.exec(b)||["",""])[1].toLowerCase();d=ia[d]||ia._default;c.innerHTML=d[1]+b.replace(Re,"<$1></$2>")+d[2];for(d=d[0];d--;)c=c.lastChild;f=db(f,c.childNodes);c=e.firstChild;c.textContent=""}else f.push(a.createTextNode(b));e.textContent="";e.innerHTML="";r(f,function(a){e.appendChild(a)}); +return e}function V(b){if(b instanceof V)return b;var a;C(b)&&(b=ba(b),a=!0);if(!(this instanceof V)){if(a&&"<"!=b.charAt(0))throw Hb("nosel");return new V(b)}if(a){a=Y;var c;b=(c=Se.exec(b))?[a.createElement(c[1])]:(c=sc(b,a))?c.childNodes:[]}tc(this,b)}function Ib(b){return b.cloneNode(!0)}function lb(b,a){a||mb(b);if(b.querySelectorAll)for(var c=b.querySelectorAll("*"),d=0,e=c.length;d<e;d++)mb(c[d])}function uc(b,a,c,d){if(B(d))throw Hb("offargs");var e=(d=nb(b))&&d.events;if(d&&d.handle)if(a)r(a.split(" "), +function(a){F(c)?(b.removeEventListener(a,e[a],!1),delete e[a]):Ra(e[a]||[],c)});else for(a in e)"$destroy"!==a&&b.removeEventListener(a,e[a],!1),delete e[a]}function mb(b,a){var c=b.ng339,d=c&&ob[c];d&&(a?delete d.data[a]:(d.handle&&(d.events.$destroy&&d.handle({},"$destroy"),uc(b)),delete ob[c],b.ng339=s))}function nb(b,a){var c=b.ng339,c=c&&ob[c];a&&!c&&(b.ng339=c=++Te,c=ob[c]={events:{},data:{},handle:s});return c}function Jb(b,a,c){if(rc(b)){var d=B(c),e=!d&&a&&!S(a),f=!a;b=(b=nb(b,!e))&&b.data; +if(d)b[a]=c;else{if(f)return b;if(e)return b&&b[a];E(b,a)}}}function jb(b,a){return b.getAttribute?-1<(" "+(b.getAttribute("class")||"")+" ").replace(/[\n\t]/g," ").indexOf(" "+a+" "):!1}function Kb(b,a){a&&b.setAttribute&&r(a.split(" "),function(a){b.setAttribute("class",ba((" "+(b.getAttribute("class")||"")+" ").replace(/[\n\t]/g," ").replace(" "+ba(a)+" "," ")))})}function Lb(b,a){if(a&&b.setAttribute){var c=(" "+(b.getAttribute("class")||"")+" ").replace(/[\n\t]/g," ");r(a.split(" "),function(a){a= +ba(a);-1===c.indexOf(" "+a+" ")&&(c+=a+" ")});b.setAttribute("class",ba(c))}}function tc(b,a){if(a)if(a.nodeType)b[b.length++]=a;else{var c=a.length;if("number"===typeof c&&a.window!==a){if(c)for(var d=0;d<c;d++)b[b.length++]=a[d]}else b[b.length++]=a}}function vc(b,a){return pb(b,"$"+(a||"ngController")+"Controller")}function pb(b,a,c){9==b.nodeType&&(b=b.documentElement);for(a=L(a)?a:[a];b;){for(var d=0,e=a.length;d<e;d++)if((c=G.data(b,a[d]))!==s)return c;b=b.parentNode||11===b.nodeType&&b.host}} +function wc(b){for(lb(b,!0);b.firstChild;)b.removeChild(b.firstChild)}function xc(b,a){a||lb(b);var c=b.parentNode;c&&c.removeChild(b)}function yc(b,a){var c=qb[a.toLowerCase()];return c&&zc[pa(b)]&&c}function Ue(b,a){var c=b.nodeName;return("INPUT"===c||"TEXTAREA"===c)&&Ac[a]}function Ve(b,a){var c=function(c,e){c.isDefaultPrevented=function(){return c.defaultPrevented};var f=a[e||c.type],g=f?f.length:0;if(g){1<g&&(f=qa(f));for(var h=0;h<g;h++)f[h].call(b,c)}};c.elem=b;return c}function Ka(b,a){var c= +b&&b.$$hashKey;if(c)return"function"===typeof c&&(c=b.$$hashKey()),c;c=typeof b;return c="function"==c||"object"==c&&null!==b?b.$$hashKey=c+":"+(a||pd)():c+":"+b}function Xa(b,a){if(a){var c=0;this.nextUid=function(){return++c}}r(b,this.put,this)}function We(b){return(b=b.toString().replace(Bc,"").match(Cc))?"function("+(b[1]||"").replace(/[\s\r\n]+/," ")+")":"fn"}function Mb(b,a,c){var d;if("function"===typeof b){if(!(d=b.$inject)){d=[];if(b.length){if(a)throw C(c)&&c||(c=b.name||We(b)),La("strictdi", +c);a=b.toString().replace(Bc,"");a=a.match(Cc);r(a[1].split(Xe),function(a){a.replace(Ye,function(a,b,c){d.push(c)})})}b.$inject=d}}else L(b)?(a=b.length-1,gb(b[a],"fn"),d=b.slice(0,a)):gb(b,"fn",!0);return d}function Cb(b,a){function c(a){return function(b,c){if(S(b))r(b,$b(a));else return a(b,c)}}function d(a,b){Ja(a,"service");if(D(b)||L(b))b=p.instantiate(b);if(!b.$get)throw La("pget",a);return n[a+"Provider"]=b}function e(a,b){return d(a,{$get:b})}function f(a){var b=[],c;r(a,function(a){function d(a){var b, +c;b=0;for(c=a.length;b<c;b++){var e=a[b],f=p.get(e[0]);f[e[1]].apply(f,e[2])}}if(!k.get(a)){k.put(a,!0);try{C(a)?(c=Va(a),b=b.concat(f(c.requires)).concat(c._runBlocks),d(c._invokeQueue),d(c._configBlocks)):D(a)?b.push(p.invoke(a)):L(a)?b.push(p.invoke(a)):gb(a,"module")}catch(e){throw L(a)&&(a=a[a.length-1]),e.message&&e.stack&&-1==e.stack.indexOf(e.message)&&(e=e.message+"\n"+e.stack),La("modulerr",a,e.stack||e.message||e);}}});return b}function g(b,c){function d(a){if(b.hasOwnProperty(a)){if(b[a]=== +h)throw La("cdep",a+" <- "+m.join(" <- "));return b[a]}try{return m.unshift(a),b[a]=h,b[a]=c(a)}catch(e){throw b[a]===h&&delete b[a],e;}finally{m.shift()}}function e(b,c,f,h){"string"===typeof f&&(h=f,f=null);var g=[];h=Mb(b,a,h);var k,m,l;m=0;for(k=h.length;m<k;m++){l=h[m];if("string"!==typeof l)throw La("itkn",l);g.push(f&&f.hasOwnProperty(l)?f[l]:d(l))}L(b)&&(b=b[k]);return b.apply(c,g)}return{invoke:e,instantiate:function(a,b,c){var d=function(){};d.prototype=(L(a)?a[a.length-1]:a).prototype; +d=new d;a=e(a,d,b,c);return S(a)||D(a)?a:d},get:d,annotate:Mb,has:function(a){return n.hasOwnProperty(a+"Provider")||b.hasOwnProperty(a)}}}a=!0===a;var h={},m=[],k=new Xa([],!0),n={$provide:{provider:c(d),factory:c(e),service:c(function(a,b){return e(a,["$injector",function(a){return a.instantiate(b)}])}),value:c(function(a,b){return e(a,da(b))}),constant:c(function(a,b){Ja(a,"constant");n[a]=b;l[a]=b}),decorator:function(a,b){var c=p.get(a+"Provider"),d=c.$get;c.$get=function(){var a=q.invoke(d, +c);return q.invoke(b,null,{$delegate:a})}}}},p=n.$injector=g(n,function(){throw La("unpr",m.join(" <- "));}),l={},q=l.$injector=g(l,function(a){var b=p.get(a+"Provider");return q.invoke(b.$get,b,s,a)});r(f(b),function(a){q.invoke(a||w)});return q}function ne(){var b=!0;this.disableAutoScrolling=function(){b=!1};this.$get=["$window","$location","$rootScope",function(a,c,d){function e(a){var b=null;r(a,function(a){b||"a"!==pa(a)||(b=a)});return b}function f(){var b=c.hash(),d;b?(d=g.getElementById(b))? +d.scrollIntoView():(d=e(g.getElementsByName(b)))?d.scrollIntoView():"top"===b&&a.scrollTo(0,0):a.scrollTo(0,0)}var g=a.document;b&&d.$watch(function(){return c.hash()},function(){d.$evalAsync(f)});return f}]}function Ne(){this.$get=["$$rAF","$timeout",function(b,a){return b.supported?function(a){return b(a)}:function(b){return a(b,0,!1)}}]}function Ze(b,a,c,d){function e(a){try{a.apply(null,Ta.call(arguments,1))}finally{if(A--,0===A)for(;u.length;)try{u.pop()()}catch(b){c.error(b)}}}function f(a, +b){(function R(){r(x,function(a){a()});z=b(R,a)})()}function g(){y=null;T!=h.url()&&(T=h.url(),r(Q,function(a){a(h.url())}))}var h=this,m=a[0],k=b.location,n=b.history,p=b.setTimeout,l=b.clearTimeout,q={};h.isMock=!1;var A=0,u=[];h.$$completeOutstandingRequest=e;h.$$incOutstandingRequestCount=function(){A++};h.notifyWhenNoOutstandingRequests=function(a){r(x,function(a){a()});0===A?a():u.push(a)};var x=[],z;h.addPollFn=function(a){F(z)&&f(100,p);x.push(a);return a};var T=k.href,v=a.find("base"),y= +null;h.url=function(a,c){k!==b.location&&(k=b.location);n!==b.history&&(n=b.history);if(a){if(T!=a)return T=a,d.history?c?n.replaceState(null,"",a):(n.pushState(null,"",a),v.attr("href",v.attr("href"))):(y=a,c?k.replace(a):k.href=a),h}else return y||k.href.replace(/%27/g,"'")};var Q=[],ca=!1;h.onUrlChange=function(a){if(!ca){if(d.history)G(b).on("popstate",g);if(d.hashchange)G(b).on("hashchange",g);else h.addPollFn(g);ca=!0}Q.push(a);return a};h.$$checkUrlChange=g;h.baseHref=function(){var a=v.attr("href"); +return a?a.replace(/^(https?\:)?\/\/[^\/]*/,""):""};var J={},N="",M=h.baseHref();h.cookies=function(a,b){var d,e,f,h;if(a)b===s?m.cookie=encodeURIComponent(a)+"=;path="+M+";expires=Thu, 01 Jan 1970 00:00:00 GMT":C(b)&&(d=(m.cookie=encodeURIComponent(a)+"="+encodeURIComponent(b)+";path="+M).length+1,4096<d&&c.warn("Cookie '"+a+"' possibly not set or overflowed because it was too large ("+d+" > 4096 bytes)!"));else{if(m.cookie!==N)for(N=m.cookie,d=N.split("; "),J={},f=0;f<d.length;f++)e=d[f],h=e.indexOf("="), +0<h&&(a=decodeURIComponent(e.substring(0,h)),J[a]===s&&(J[a]=decodeURIComponent(e.substring(h+1))));return J}};h.defer=function(a,b){var c;A++;c=p(function(){delete q[c];e(a)},b||0);q[c]=!0;return c};h.defer.cancel=function(a){return q[a]?(delete q[a],l(a),e(w),!0):!1}}function pe(){this.$get=["$window","$log","$sniffer","$document",function(b,a,c,d){return new Ze(b,d,a,c)}]}function qe(){this.$get=function(){function b(b,d){function e(a){a!=p&&(l?l==a&&(l=a.n):l=a,f(a.n,a.p),f(a,p),p=a,p.n=null)} +function f(a,b){a!=b&&(a&&(a.p=b),b&&(b.n=a))}if(b in a)throw K("$cacheFactory")("iid",b);var g=0,h=E({},d,{id:b}),m={},k=d&&d.capacity||Number.MAX_VALUE,n={},p=null,l=null;return a[b]={put:function(a,b){if(k<Number.MAX_VALUE){var c=n[a]||(n[a]={key:a});e(c)}if(!F(b))return a in m||g++,m[a]=b,g>k&&this.remove(l.key),b},get:function(a){if(k<Number.MAX_VALUE){var b=n[a];if(!b)return;e(b)}return m[a]},remove:function(a){if(k<Number.MAX_VALUE){var b=n[a];if(!b)return;b==p&&(p=b.p);b==l&&(l=b.n);f(b.n, +b.p);delete n[a]}delete m[a];g--},removeAll:function(){m={};g=0;n={};p=l=null},destroy:function(){n=h=m=null;delete a[b]},info:function(){return E({},h,{size:g})}}}var a={};b.info=function(){var b={};r(a,function(a,e){b[e]=a.info()});return b};b.get=function(b){return a[b]};return b}}function He(){this.$get=["$cacheFactory",function(b){return b("templates")}]}function jc(b,a){var c={},d=/^\s*directive\:\s*([\d\w_\-]+)\s+(.*)$/,e=/(([\d\w_\-]+)(?:\:([^;]+))?;?)/,f=rd("ngSrc,ngSrcset,src,srcset"),g= +/^(on[a-z]+|formaction)$/;this.directive=function k(a,d){Ja(a,"directive");C(a)?(Fb(d,"directiveFactory"),c.hasOwnProperty(a)||(c[a]=[],b.factory(a+"Directive",["$injector","$exceptionHandler",function(b,d){var e=[];r(c[a],function(c,f){try{var h=b.invoke(c);D(h)?h={compile:da(h)}:!h.compile&&h.link&&(h.compile=da(h.link));h.priority=h.priority||0;h.index=f;h.name=h.name||a;h.require=h.require||h.controller&&h.name;h.restrict=h.restrict||"EA";e.push(h)}catch(g){d(g)}});return e}])),c[a].push(d)): +r(a,$b(k));return this};this.aHrefSanitizationWhitelist=function(b){return B(b)?(a.aHrefSanitizationWhitelist(b),this):a.aHrefSanitizationWhitelist()};this.imgSrcSanitizationWhitelist=function(b){return B(b)?(a.imgSrcSanitizationWhitelist(b),this):a.imgSrcSanitizationWhitelist()};var h=!0;this.debugInfoEnabled=function(a){return B(a)?(h=a,this):h};this.$get=["$injector","$interpolate","$exceptionHandler","$templateRequest","$parse","$controller","$rootScope","$document","$sce","$animate","$$sanitizeUri", +function(a,b,p,l,q,A,u,x,z,T,v){function y(a,b){try{a.addClass(b)}catch(c){}}function Q(a,b,c,d,e){a instanceof G||(a=G(a));r(a,function(b,c){3==b.nodeType&&b.nodeValue.match(/\S+/)&&(a[c]=G(b).wrap("<span></span>").parent()[0])});var f=ca(a,b,a,c,d,e);Q.$$addScopeClass(a);var h=null,g=a,k;return function(b,c,d,e,l){Fb(b,"scope");h||(h=(l=l&&l[0])?"foreignobject"!==pa(l)&&l.toString().match(/SVG/)?"svg":"html":"html");"html"!==h&&a[0]!==k&&(g=G(Nb(h,G("<div>").append(a).html())));k=a[0];l=c?Ia.clone.call(g): +g;if(d)for(var n in d)l.data("$"+n+"Controller",d[n].instance);Q.$$addScopeInfo(l,b);c&&c(l,b);f&&f(b,l,l,e);return l}}function ca(a,b,c,d,e,f){function h(a,c,d,e){var f,k,l,n,u,q,ua;if(p)for(ua=Array(c.length),n=0;n<g.length;n+=3)f=g[n],ua[f]=c[f];else ua=c;n=0;for(u=g.length;n<u;)k=ua[g[n++]],c=g[n++],f=g[n++],c?(c.scope?(l=a.$new(),Q.$$addScopeInfo(G(k),l)):l=a,q=c.transcludeOnThisElement?J(a,c.transclude,e,c.elementTranscludeOnThisElement):!c.templateOnThisElement&&e?e:!e&&b?J(a,b):null,c(f,l, +k,d,q)):f&&f(a,k.childNodes,s,e)}for(var g=[],k,l,n,u,p,q=0;q<a.length;q++){k=new Ob;l=N(a[q],[],k,0===q?d:s,e);(f=l.length?H(l,a[q],k,b,c,null,[],[],f):null)&&f.scope&&Q.$$addScopeClass(k.$$element);k=f&&f.terminal||!(n=a[q].childNodes)||!n.length?null:ca(n,f?(f.transcludeOnThisElement||!f.templateOnThisElement)&&f.transclude:b);if(f||k)g.push(q,f,k),u=!0,p=p||f;f=null}return u?h:null}function J(a,b,c,d){return function(e,f,h,g){var k=!1;e||(e=a.$new(),k=e.$$transcluded=!0);f=b(e,f,h,c,g);if(k&& +!d)f.on("$destroy",function(){e.$destroy()});return f}}function N(b,f,h,g,l){var n=h.$attr,u;switch(b.nodeType){case 1:R(f,va(pa(b)),"E",g,l);for(var p,q,T,A=b.attributes,z=0,v=A&&A.length;z<v;z++){var x=!1,J=!1;p=A[z];if(!X||8<=X||p.specified){u=p.name;p=ba(p.value);q=va(u);if(T=U.test(q))u=Db(q.substr(6),"-");var W=q.replace(/(Start|End)$/,""),r;a:{var H=W;if(c.hasOwnProperty(H)){r=void 0;for(var H=a.get(H+"Directive"),O=0,Q=H.length;O<Q;O++)if(r=H[O],r.multiElement){r=!0;break a}}r=!1}r&&q===W+ +"Start"&&(x=u,J=u.substr(0,u.length-5)+"end",u=u.substr(0,u.length-6));q=va(u.toLowerCase());n[q]=u;if(T||!h.hasOwnProperty(q))h[q]=p,yc(b,q)&&(h[q]=!0);ya(b,f,p,q,T);R(f,q,"A",g,l,x,J)}}b=b.className;if(C(b)&&""!==b)for(;u=e.exec(b);)q=va(u[2]),R(f,q,"C",g,l)&&(h[q]=ba(u[3])),b=b.substr(u.index+u[0].length);break;case 3:t(f,b.nodeValue);break;case 8:try{if(u=d.exec(b.nodeValue))q=va(u[1]),R(f,q,"M",g,l)&&(h[q]=ba(u[2]))}catch(M){}}f.sort(F);return f}function M(a,b,c){var d=[],e=0;if(b&&a.hasAttribute&& +a.hasAttribute(b)){do{if(!a)throw ja("uterdir",b,c);1==a.nodeType&&(a.hasAttribute(b)&&e++,a.hasAttribute(c)&&e--);d.push(a);a=a.nextSibling}while(0<e)}else d.push(a);return G(d)}function O(a,b,c){return function(d,e,f,h,g){e=M(e[0],b,c);return a(d,e,f,h,g)}}function H(a,c,d,e,f,h,g,k,l){function u(a,b,c,d){if(a){c&&(a=O(a,c,d));a.require=I.require;a.directiveName=ha;if(y===I||I.$$isolateScope)a=ga(a,{isolateScope:!0});g.push(a)}if(b){c&&(b=O(b,c,d));b.require=I.require;b.directiveName=ha;if(y=== +I||I.$$isolateScope)b=ga(b,{isolateScope:!0});k.push(b)}}function T(a,b,c,d){var e,f="data",h=!1;if(C(b)){for(;"^"==(e=b.charAt(0))||"?"==e;)b=b.substr(1),"^"==e&&(f="inheritedData"),h=h||"?"==e;e=null;d&&"data"===f&&(e=d[b])&&(e=e.instance);e=e||c[f]("$"+b+"Controller");if(!e&&!h)throw ja("ctreq",b,a);}else L(b)&&(e=[],r(b,function(b){e.push(T(a,b,c,d))}));return e}function z(a,e,f,h,l){function u(a,b,c){var d;Qa(a)||(c=b,b=a,a=s);E&&(d=W);c||(c=E?M.parent():M);return l(a,b,d,c)}var p,v,ua,x,W,O, +M,R;c===f?(R=d,M=d.$$element):(M=G(f),R=new Ob(M,d));y&&(x=e.$new(!0));O=l&&u;J&&(H={},W={},r(J,function(a){var b={$scope:a===y||a.$$isolateScope?x:e,$element:M,$attrs:R,$transclude:O};ua=a.controller;"@"==ua&&(ua=R[a.name]);b=A(ua,b,!0,a.controllerAs);W[a.name]=b;E||M.data("$"+a.name+"Controller",b.instance);H[a.name]=b}));if(y){var N=/^\s*([@=&])(\??)\s*(\w*)\s*$/;Q.$$addScopeInfo(M,x,!0,!(ca&&(ca===y||ca===y.$$originalDirective)));Q.$$addScopeClass(M,!0);h=H&&H[y.name];var xa=x;h&&h.identifier&& +!0===y.bindToController&&(xa=h.instance);r(y.scope,function(a,c){var d=a.match(N)||[],f=d[3]||c,h="?"==d[2],d=d[1],g,k,l,u;x.$$isolateBindings[c]=d+f;switch(d){case "@":R.$observe(f,function(a){x[c]=a});R.$$observers[f].$$scope=e;R[f]&&(xa[c]=b(R[f])(e));break;case "=":if(h&&!R[f])break;k=q(R[f]);u=k.literal?ra:function(a,b){return a===b||a!==a&&b!==b};l=k.assign||function(){g=xa[c]=k(e);throw ja("nonassign",R[f],y.name);};g=xa[c]=k(e);h=e.$watch(q(R[f],function(a){u(a,xa[c])||(u(a,g)?l(e,a=xa[c]): +xa[c]=a);return g=a}),null,k.literal);x.$on("$destroy",h);break;case "&":k=q(R[f]);xa[c]=function(a){return k(e,a)};break;default:throw ja("iscp",y.name,c,a);}})}H&&(r(H,function(a){a()}),H=null);h=0;for(p=g.length;h<p;h++)v=g[h],Dc(v,v.isolateScope?x:e,M,R,v.require&&T(v.directiveName,v.require,M,W),O);h=e;y&&(y.template||null===y.templateUrl)&&(h=x);a&&a(h,f.childNodes,s,l);for(h=k.length-1;0<=h;h--)v=k[h],Dc(v,v.isolateScope?x:e,M,R,v.require&&T(v.directiveName,v.require,M,W),O)}l=l||{};for(var v= +-Number.MAX_VALUE,x,J=l.controllerDirectives,H,y=l.newIsolateScopeDirective,ca=l.templateDirective,R=l.nonTlbTranscludeDirective,w=!1,F=!1,E=l.hasElementTranscludeDirective,aa=d.$$element=G(c),I,ha,t,P=e,za,ma=0,ya=a.length;ma<ya;ma++){I=a[ma];var U=I.$$start,X=I.$$end;U&&(aa=M(c,U,X));t=s;if(v>I.priority)break;if(t=I.scope)I.templateUrl||(S(t)?(K("new/isolated scope",y||x,I,aa),y=I):K("new/isolated scope",y,I,aa)),x=x||I;ha=I.name;!I.templateUrl&&I.controller&&(t=I.controller,J=J||{},K("'"+ha+"' controller", +J[ha],I,aa),J[ha]=I);if(t=I.transclude)w=!0,I.$$tlb||(K("transclusion",R,I,aa),R=I),"element"==t?(E=!0,v=I.priority,t=aa,aa=d.$$element=G(Y.createComment(" "+ha+": "+d[ha]+" ")),c=aa[0],rb(f,Ta.call(t,0),c),P=Q(t,e,v,h&&h.name,{nonTlbTranscludeDirective:R})):(t=G(Ib(c)).contents(),aa.empty(),P=Q(t,e));if(I.template)if(F=!0,K("template",ca,I,aa),ca=I,t=D(I.template)?I.template(aa,d):I.template,t=V(t),I.replace){h=I;t=Gb.test(t)?G(Nb(I.templateNamespace,ba(t))):[];c=t[0];if(1!=t.length||1!==c.nodeType)throw ja("tplrt", +ha,"");rb(f,aa,c);ya={$attr:{}};t=N(c,[],ya);var Z=a.splice(ma+1,a.length-(ma+1));y&&W(t);a=a.concat(t).concat(Z);B(d,ya);ya=a.length}else aa.html(t);if(I.templateUrl)F=!0,K("template",ca,I,aa),ca=I,I.replace&&(h=I),z=$e(a.splice(ma,a.length-ma),aa,d,f,w&&P,g,k,{controllerDirectives:J,newIsolateScopeDirective:y,templateDirective:ca,nonTlbTranscludeDirective:R}),ya=a.length;else if(I.compile)try{za=I.compile(aa,d,P),D(za)?u(null,za,U,X):za&&u(za.pre,za.post,U,X)}catch($){p($,ta(aa))}I.terminal&&(z.terminal= +!0,v=Math.max(v,I.priority))}z.scope=x&&!0===x.scope;z.transcludeOnThisElement=w;z.elementTranscludeOnThisElement=E;z.templateOnThisElement=F;z.transclude=P;l.hasElementTranscludeDirective=E;return z}function W(a){for(var b=0,c=a.length;b<c;b++)a[b]=bc(a[b],{$$isolateScope:!0})}function R(b,d,e,f,h,g,l){if(d===h)return null;h=null;if(c.hasOwnProperty(d)){var n;d=a.get(d+"Directive");for(var u=0,q=d.length;u<q;u++)try{n=d[u],(f===s||f>n.priority)&&-1!=n.restrict.indexOf(e)&&(g&&(n=bc(n,{$$start:g, +$$end:l})),b.push(n),h=n)}catch(T){p(T)}}return h}function B(a,b){var c=b.$attr,d=a.$attr,e=a.$$element;r(a,function(d,e){"$"!=e.charAt(0)&&(b[e]&&b[e]!==d&&(d+=("style"===e?";":" ")+b[e]),a.$set(e,d,!0,c[e]))});r(b,function(b,f){"class"==f?(y(e,b),a["class"]=(a["class"]?a["class"]+" ":"")+b):"style"==f?(e.attr("style",e.attr("style")+";"+b),a.style=(a.style?a.style+";":"")+b):"$"==f.charAt(0)||a.hasOwnProperty(f)||(a[f]=b,d[f]=c[f])})}function $e(a,b,c,d,e,f,h,g){var k=[],n,u,p=b[0],q=a.shift(), +T=E({},q,{templateUrl:null,transclude:null,replace:null,$$originalDirective:q}),v=D(q.templateUrl)?q.templateUrl(b,c):q.templateUrl,A=q.templateNamespace;b.empty();l(z.getTrustedResourceUrl(v)).then(function(l){var z,x;l=V(l);if(q.replace){l=Gb.test(l)?G(Nb(A,ba(l))):[];z=l[0];if(1!=l.length||1!==z.nodeType)throw ja("tplrt",q.name,v);l={$attr:{}};rb(d,b,z);var O=N(z,[],l);S(q.scope)&&W(O);a=O.concat(a);B(c,l)}else z=p,b.html(l);a.unshift(T);n=H(a,z,c,e,b,q,f,h,g);r(d,function(a,c){a==z&&(d[c]=b[0])}); +for(u=ca(b[0].childNodes,e);k.length;){l=k.shift();x=k.shift();var M=k.shift(),Q=k.shift(),O=b[0];if(x!==p){var R=x.className;g.hasElementTranscludeDirective&&q.replace||(O=Ib(z));rb(M,G(x),O);y(G(O),R)}x=n.transcludeOnThisElement?J(l,n.transclude,Q):Q;n(u,l,O,d,x)}k=null});return function(a,b,c,d,e){a=e;k?(k.push(b),k.push(c),k.push(d),k.push(a)):(n.transcludeOnThisElement&&(a=J(b,n.transclude,e)),n(u,b,c,d,a))}}function F(a,b){var c=b.priority-a.priority;return 0!==c?c:a.name!==b.name?a.name<b.name? +-1:1:a.index-b.index}function K(a,b,c,d){if(b)throw ja("multidir",b.name,c.name,a,ta(d));}function t(a,c){var d=b(c,!0);d&&a.push({priority:0,compile:function(a){a=a.parent();var b=!!a.length;b&&Q.$$addBindingClass(a);return function(a,c){var e=c.parent();b||Q.$$addBindingClass(e);Q.$$addBindingInfo(e,d.expressions);a.$watch(d,function(a){c[0].nodeValue=a})}}})}function Nb(a,b){a=P(a||"html");switch(a){case "svg":case "math":var c=Y.createElement("div");c.innerHTML="<"+a+">"+b+"</"+a+">";return c.childNodes[0].childNodes; +default:return b}}function za(a,b){if("srcdoc"==b)return z.HTML;var c=pa(a);if("xlinkHref"==b||"form"==c&&"action"==b||"img"!=c&&("src"==b||"ngSrc"==b))return z.RESOURCE_URL}function ya(a,c,d,e,h){var k=b(d,!0);if(k){if("multiple"===e&&"select"===pa(a))throw ja("selmulti",ta(a));c.push({priority:100,compile:function(){return{pre:function(c,d,l){d=l.$$observers||(l.$$observers={});if(g.test(e))throw ja("nodomevents");if(k=b(l[e],!0,za(a,e),f[e]||h))l[e]=k(c),(d[e]||(d[e]=[])).$$inter=!0,(l.$$observers&& +l.$$observers[e].$$scope||c).$watch(k,function(a,b){"class"===e&&a!=b?l.$updateClass(a,b):l.$set(e,a)})}}}})}}function rb(a,b,c){var d=b[0],e=b.length,f=d.parentNode,h,g;if(a)for(h=0,g=a.length;h<g;h++)if(a[h]==d){a[h++]=c;g=h+e-1;for(var k=a.length;h<k;h++,g++)g<k?a[h]=a[g]:delete a[h];a.length-=e-1;a.context===d&&(a.context=c);break}f&&f.replaceChild(c,d);a=Y.createDocumentFragment();a.appendChild(d);G(c).data(G(d).data());la?(Eb=!0,la.cleanData([d])):delete G.cache[d[G.expando]];d=1;for(e=b.length;d< +e;d++)f=b[d],G(f).remove(),a.appendChild(f),delete b[d];b[0]=c;b.length=1}function ga(a,b){return E(function(){return a.apply(null,arguments)},a,b)}function Dc(a,b,c,d,e,f){try{a(b,c,d,e,f)}catch(h){p(h,ta(c))}}var Ob=function(a,b){if(b){var c=Object.keys(b),d,e,f;d=0;for(e=c.length;d<e;d++)f=c[d],this[f]=b[f]}else this.$attr={};this.$$element=a};Ob.prototype={$normalize:va,$addClass:function(a){a&&0<a.length&&T.addClass(this.$$element,a)},$removeClass:function(a){a&&0<a.length&&T.removeClass(this.$$element, +a)},$updateClass:function(a,b){var c=Ec(a,b);c&&c.length&&T.addClass(this.$$element,c);(c=Ec(b,a))&&c.length&&T.removeClass(this.$$element,c)},$set:function(a,b,c,d){var e=this.$$element[0],f=yc(e,a),h=Ue(e,a),e=a;f?(this.$$element.prop(a,b),d=f):h&&(this[h]=b,e=h);this[a]=b;d?this.$attr[a]=d:(d=this.$attr[a])||(this.$attr[a]=d=Db(a,"-"));f=pa(this.$$element);if("a"===f&&"href"===a||"img"===f&&"src"===a)this[a]=b=v(b,"src"===a);!1!==c&&(null===b||b===s?this.$$element.removeAttr(d):this.$$element.attr(d, +b));(a=this.$$observers)&&r(a[e],function(a){try{a(b)}catch(c){p(c)}})},$observe:function(a,b){var c=this,d=c.$$observers||(c.$$observers={}),e=d[a]||(d[a]=[]);e.push(b);u.$evalAsync(function(){e.$$inter||b(c[a])});return function(){Ra(e,b)}}};var ma=b.startSymbol(),ha=b.endSymbol(),V="{{"==ma||"}}"==ha?Pa:function(a){return a.replace(/\{\{/g,ma).replace(/}}/g,ha)},U=/^ngAttr[A-Z]/;Q.$$addBindingInfo=h?function(a,b){var c=a.data("$binding")||[];L(b)?c=c.concat(b):c.push(b);a.data("$binding",c)}:w; +Q.$$addBindingClass=h?function(a){y(a,"ng-binding")}:w;Q.$$addScopeInfo=h?function(a,b,c,d){a.data(c?d?"$isolateScopeNoTemplate":"$isolateScope":"$scope",b)}:w;Q.$$addScopeClass=h?function(a,b){y(a,b?"ng-isolate-scope":"ng-scope")}:w;return Q}]}function va(b){return Wa(b.replace(af,""))}function Ec(b,a){var c="",d=b.split(/\s+/),e=a.split(/\s+/),f=0;a:for(;f<d.length;f++){for(var g=d[f],h=0;h<e.length;h++)if(g==e[h])continue a;c+=(0<c.length?" ":"")+g}return c}function re(){var b={},a=!1,c=/^(\S+)(\s+as\s+(\w+))?$/; +this.register=function(a,c){Ja(a,"controller");S(a)?E(b,a):b[a]=c};this.allowGlobals=function(){a=!0};this.$get=["$injector","$window",function(d,e){function f(a,b,c,d){if(!a||!S(a.$scope))throw K("$controller")("noscp",d,b);a.$scope[b]=c}return function(g,h,m,k){var n,p,l;m=!0===m;k&&C(k)&&(l=k);C(g)&&(k=g.match(c),p=k[1],l=l||k[3],g=b.hasOwnProperty(p)?b[p]:ic(h.$scope,p,!0)||(a?ic(e,p,!0):s),gb(g,p,!0));if(m)return m=function(){},m.prototype=(L(g)?g[g.length-1]:g).prototype,n=new m,l&&f(h,l,n, +p||g.name),E(function(){d.invoke(g,n,h,p);return n},{instance:n,identifier:l});n=d.instantiate(g,h,p);l&&f(h,l,n,p||g.name);return n}}]}function se(){this.$get=["$window",function(b){return G(b.document)}]}function te(){this.$get=["$log",function(b){return function(a,c){b.error.apply(b,arguments)}}]}function Fc(b){var a={},c,d,e;if(!b)return a;r(b.split("\n"),function(b){e=b.indexOf(":");c=P(ba(b.substr(0,e)));d=ba(b.substr(e+1));c&&(a[c]=a[c]?a[c]+", "+d:d)});return a}function Gc(b){var a=S(b)?b: +s;return function(c){a||(a=Fc(b));return c?a[P(c)]||null:a}}function Hc(b,a,c){if(D(c))return c(b,a);r(c,function(c){b=c(b,a)});return b}function we(){var b=/^\s*(\[|\{[^\{])/,a=/[\}\]]\s*$/,c=/^\)\]\}',?\n/,d={"Content-Type":"application/json;charset=utf-8"},e=this.defaults={transformResponse:[function(d){C(d)&&(d=d.replace(c,""),b.test(d)&&a.test(d)&&(d=dc(d)));return d}],transformRequest:[function(a){return S(a)&&"[object File]"!==Ga.call(a)&&"[object Blob]"!==Ga.call(a)?sa(a):a}],headers:{common:{Accept:"application/json, text/plain, */*"}, +post:qa(d),put:qa(d),patch:qa(d)},xsrfCookieName:"XSRF-TOKEN",xsrfHeaderName:"X-XSRF-TOKEN"},f=!1;this.useApplyAsync=function(a){return B(a)?(f=!!a,this):f};var g=this.interceptors=[];this.$get=["$httpBackend","$browser","$cacheFactory","$rootScope","$q","$injector",function(a,b,c,d,p,l){function q(a){function b(a){var d=E({},a,{data:Hc(a.data,a.headers,c.transformResponse)});a=a.status;return 200<=a&&300>a?d:p.reject(d)}var c={method:"get",transformRequest:e.transformRequest,transformResponse:e.transformResponse}, +d=function(a){var b=e.headers,c=E({},a.headers),d,f,b=E({},b.common,b[P(a.method)]);a:for(d in b){a=P(d);for(f in c)if(P(f)===a)continue a;c[d]=b[d]}(function(a){var b;r(a,function(c,d){D(c)&&(b=c(),null!=b?a[d]=b:delete a[d])})})(c);return c}(a);E(c,a);c.headers=d;c.method=ib(c.method);var f=[function(a){d=a.headers;var c=Hc(a.data,Gc(d),a.transformRequest);F(c)&&r(d,function(a,b){"content-type"===P(b)&&delete d[b]});F(a.withCredentials)&&!F(e.withCredentials)&&(a.withCredentials=e.withCredentials); +return A(a,c,d).then(b,b)},s],h=p.when(c);for(r(z,function(a){(a.request||a.requestError)&&f.unshift(a.request,a.requestError);(a.response||a.responseError)&&f.push(a.response,a.responseError)});f.length;){a=f.shift();var g=f.shift(),h=h.then(a,g)}h.success=function(a){h.then(function(b){a(b.data,b.status,b.headers,c)});return h};h.error=function(a){h.then(null,function(b){a(b.data,b.status,b.headers,c)});return h};return h}function A(c,g,k){function l(a,b,c,e){function h(){z(b,a,c,e)}O&&(200<=a&& +300>a?O.put(W,[a,b,Fc(c),e]):O.remove(W));f?d.$applyAsync(h):(h(),d.$$phase||d.$apply())}function z(a,b,d,e){b=Math.max(b,0);(200<=b&&300>b?r.resolve:r.reject)({data:a,status:b,headers:Gc(d),config:c,statusText:e})}function A(){var a=q.pendingRequests.indexOf(c);-1!==a&&q.pendingRequests.splice(a,1)}var r=p.defer(),M=r.promise,O,H,W=u(c.url,c.params);q.pendingRequests.push(c);M.then(A,A);!c.cache&&!e.cache||!1===c.cache||"GET"!==c.method&&"JSONP"!==c.method||(O=S(c.cache)?c.cache:S(e.cache)?e.cache: +x);if(O)if(H=O.get(W),B(H)){if(H&&D(H.then))return H.then(A,A),H;L(H)?z(H[1],H[0],qa(H[2]),H[3]):z(H,200,{},"OK")}else O.put(W,M);F(H)&&((H=Ic(c.url)?b.cookies()[c.xsrfCookieName||e.xsrfCookieName]:s)&&(k[c.xsrfHeaderName||e.xsrfHeaderName]=H),a(c.method,W,g,l,k,c.timeout,c.withCredentials,c.responseType));return M}function u(a,b){if(!b)return a;var c=[];od(b,function(a,b){null===a||F(a)||(L(a)||(a=[a]),r(a,function(a){S(a)&&(a=fa(a)?a.toISOString():sa(a));c.push(Da(b)+"="+Da(a))}))});0<c.length&& +(a+=(-1==a.indexOf("?")?"?":"&")+c.join("&"));return a}var x=c("$http"),z=[];r(g,function(a){z.unshift(C(a)?l.get(a):l.invoke(a))});q.pendingRequests=[];(function(a){r(arguments,function(a){q[a]=function(b,c){return q(E(c||{},{method:a,url:b}))}})})("get","delete","head","jsonp");(function(a){r(arguments,function(a){q[a]=function(b,c,d){return q(E(d||{},{method:a,url:b,data:c}))}})})("post","put","patch");q.defaults=e;return q}]}function bf(b){if(8>=X&&(!b.match(/^(get|post|head|put|delete|options)$/i)|| +!t.XMLHttpRequest))return new t.ActiveXObject("Microsoft.XMLHTTP");if(t.XMLHttpRequest)return new t.XMLHttpRequest;throw K("$httpBackend")("noxhr");}function xe(){this.$get=["$browser","$window","$document",function(b,a,c){return cf(b,bf,b.defer,a.angular.callbacks,c[0])}]}function cf(b,a,c,d,e){function f(a,b,c){var f=e.createElement("script"),n=null;f.type="text/javascript";f.src=a;f.async=!0;n=function(a){f.removeEventListener("load",n,!1);f.removeEventListener("error",n,!1);e.body.removeChild(f); +f=null;var g=-1,q="unknown";a&&("load"!==a.type||d[b].called||(a={type:"error"}),q=a.type,g="error"===a.type?404:200);c&&c(g,q)};f.addEventListener("load",n,!1);f.addEventListener("error",n,!1);e.body.appendChild(f);return n}return function(e,h,m,k,n,p,l,q){function A(){x=-1;T&&T();v&&v.abort()}function u(a,d,e,f,g){Q&&c.cancel(Q);T=v=null;0===d&&(d=e?200:"file"==Aa(h).protocol?404:0);a(1223===d?204:d,e,f,g||"");b.$$completeOutstandingRequest(w)}var x;b.$$incOutstandingRequestCount();h=h||b.url(); +if("jsonp"==P(e)){var z="_"+(d.counter++).toString(36);d[z]=function(a){d[z].data=a;d[z].called=!0};var T=f(h.replace("JSON_CALLBACK","angular.callbacks."+z),z,function(a,b){u(k,a,d[z].data,"",b);d[z]=w})}else{var v=a(e);v.open(e,h,!0);r(n,function(a,b){B(a)&&v.setRequestHeader(b,a)});v.onreadystatechange=function(){if(v&&4==v.readyState){var a=null,b=null,c="";-1!==x&&(a=v.getAllResponseHeaders(),b="response"in v?v.response:v.responseText);-1===x&&10>X||(c=v.statusText);u(k,x||v.status,b,a,c)}}; +l&&(v.withCredentials=!0);if(q)try{v.responseType=q}catch(y){if("json"!==q)throw y;}v.send(m||null)}if(0<p)var Q=c(A,p);else p&&D(p.then)&&p.then(A)}}function ue(){var b="{{",a="}}";this.startSymbol=function(a){return a?(b=a,this):b};this.endSymbol=function(b){return b?(a=b,this):a};this.$get=["$parse","$exceptionHandler","$sce",function(c,d,e){function f(a){return"\\\\\\"+a}function g(f,g,q,A){function u(c){return c.replace(k,b).replace(n,a)}function x(a){try{var b;var c=q?e.getTrusted(q,a):e.valueOf(a); +if(null==c)b="";else{switch(typeof c){case "string":break;case "number":c=""+c;break;default:c=sa(c)}b=c}return b}catch(h){a=Pb("interr",f,h.toString()),d(a)}}A=!!A;for(var z,T,v=0,r=[],Q=[],s=f.length,J=[],N=[];v<s;)if(-1!=(z=f.indexOf(b,v))&&-1!=(T=f.indexOf(a,z+h)))v!==z&&J.push(u(f.substring(v,z))),v=f.substring(z+h,T),r.push(v),Q.push(c(v,x)),v=T+m,N.push(J.length),J.push("");else{v!==s&&J.push(u(f.substring(v)));break}if(q&&1<J.length)throw Pb("noconcat",f);if(!g||r.length){var M=function(a){for(var b= +0,c=r.length;b<c;b++){if(A&&F(a[b]))return;J[N[b]]=a[b]}return J.join("")};return E(function(a){var b=0,c=r.length,e=Array(c);try{for(;b<c;b++)e[b]=Q[b](a);return M(e)}catch(h){a=Pb("interr",f,h.toString()),d(a)}},{exp:f,expressions:r,$$watchDelegate:function(a,b,c){var d;return a.$watchGroup(Q,function(c,e){var f=M(c);D(b)&&b.call(this,f,c!==e?d:f,a);d=f},c)}})}}var h=b.length,m=a.length,k=new RegExp(b.replace(/./g,f),"g"),n=new RegExp(a.replace(/./g,f),"g");g.startSymbol=function(){return b};g.endSymbol= +function(){return a};return g}]}function ve(){this.$get=["$rootScope","$window","$q","$$q",function(b,a,c,d){function e(e,h,m,k){var n=a.setInterval,p=a.clearInterval,l=0,q=B(k)&&!k,A=(q?d:c).defer(),u=A.promise;m=B(m)?m:0;u.then(null,null,e);u.$$intervalId=n(function(){A.notify(l++);0<m&&l>=m&&(A.resolve(l),p(u.$$intervalId),delete f[u.$$intervalId]);q||b.$apply()},h);f[u.$$intervalId]=A;return u}var f={};e.cancel=function(b){return b&&b.$$intervalId in f?(f[b.$$intervalId].reject("canceled"),a.clearInterval(b.$$intervalId), +delete f[b.$$intervalId],!0):!1};return e}]}function Dd(){this.$get=function(){return{id:"en-us",NUMBER_FORMATS:{DECIMAL_SEP:".",GROUP_SEP:",",PATTERNS:[{minInt:1,minFrac:0,maxFrac:3,posPre:"",posSuf:"",negPre:"-",negSuf:"",gSize:3,lgSize:3},{minInt:1,minFrac:2,maxFrac:2,posPre:"\u00a4",posSuf:"",negPre:"(\u00a4",negSuf:")",gSize:3,lgSize:3}],CURRENCY_SYM:"$"},DATETIME_FORMATS:{MONTH:"January February March April May June July August September October November December".split(" "),SHORTMONTH:"Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split(" "), +DAY:"Sunday Monday Tuesday Wednesday Thursday Friday Saturday".split(" "),SHORTDAY:"Sun Mon Tue Wed Thu Fri Sat".split(" "),AMPMS:["AM","PM"],medium:"MMM d, y h:mm:ss a",short:"M/d/yy h:mm a",fullDate:"EEEE, MMMM d, y",longDate:"MMMM d, y",mediumDate:"MMM d, y",shortDate:"M/d/yy",mediumTime:"h:mm:ss a",shortTime:"h:mm a"},pluralCat:function(b){return 1===b?"one":"other"}}}}function Qb(b){b=b.split("/");for(var a=b.length;a--;)b[a]=eb(b[a]);return b.join("/")}function Jc(b,a,c){b=Aa(b,c);a.$$protocol= +b.protocol;a.$$host=b.hostname;a.$$port=U(b.port)||df[b.protocol]||null}function Kc(b,a,c){var d="/"!==b.charAt(0);d&&(b="/"+b);b=Aa(b,c);a.$$path=decodeURIComponent(d&&"/"===b.pathname.charAt(0)?b.pathname.substring(1):b.pathname);a.$$search=fc(b.search);a.$$hash=decodeURIComponent(b.hash);a.$$path&&"/"!=a.$$path.charAt(0)&&(a.$$path="/"+a.$$path)}function wa(b,a){if(0===a.indexOf(b))return a.substr(b.length)}function Ya(b){var a=b.indexOf("#");return-1==a?b:b.substr(0,a)}function Rb(b){return b.substr(0, +Ya(b).lastIndexOf("/")+1)}function Lc(b,a){this.$$html5=!0;a=a||"";var c=Rb(b);Jc(b,this,b);this.$$parse=function(a){var e=wa(c,a);if(!C(e))throw sb("ipthprfx",a,c);Kc(e,this,b);this.$$path||(this.$$path="/");this.$$compose()};this.$$compose=function(){var a=Bb(this.$$search),b=this.$$hash?"#"+eb(this.$$hash):"";this.$$url=Qb(this.$$path)+(a?"?"+a:"")+b;this.$$absUrl=c+this.$$url.substr(1)};this.$$parseLinkUrl=function(d,e){if(e&&"#"===e[0])return this.hash(e.slice(1)),!0;var f,g;(f=wa(b,d))!==s? +(g=f,g=(f=wa(a,f))!==s?c+(wa("/",f)||f):b+g):(f=wa(c,d))!==s?g=c+f:c==d+"/"&&(g=c);g&&this.$$parse(g);return!!g}}function Sb(b,a){var c=Rb(b);Jc(b,this,b);this.$$parse=function(d){var e=wa(b,d)||wa(c,d),e="#"==e.charAt(0)?wa(a,e):this.$$html5?e:"";if(!C(e))throw sb("ihshprfx",d,a);Kc(e,this,b);d=this.$$path;var f=/^\/[A-Z]:(\/.*)/;0===e.indexOf(b)&&(e=e.replace(b,""));f.exec(e)||(d=(e=f.exec(d))?e[1]:d);this.$$path=d;this.$$compose()};this.$$compose=function(){var c=Bb(this.$$search),e=this.$$hash? +"#"+eb(this.$$hash):"";this.$$url=Qb(this.$$path)+(c?"?"+c:"")+e;this.$$absUrl=b+(this.$$url?a+this.$$url:"")};this.$$parseLinkUrl=function(a,c){return Ya(b)==Ya(a)?(this.$$parse(a),!0):!1}}function Mc(b,a){this.$$html5=!0;Sb.apply(this,arguments);var c=Rb(b);this.$$parseLinkUrl=function(d,e){if(e&&"#"===e[0])return this.hash(e.slice(1)),!0;var f,g;b==Ya(d)?f=d:(g=wa(c,d))?f=b+a+g:c===d+"/"&&(f=c);f&&this.$$parse(f);return!!f};this.$$compose=function(){var c=Bb(this.$$search),e=this.$$hash?"#"+eb(this.$$hash): +"";this.$$url=Qb(this.$$path)+(c?"?"+c:"")+e;this.$$absUrl=b+a+this.$$url}}function tb(b){return function(){return this[b]}}function Nc(b,a){return function(c){if(F(c))return this[b];this[b]=a(c);this.$$compose();return this}}function ye(){var b="",a=!1;this.hashPrefix=function(a){return B(a)?(b=a,this):b};this.html5Mode=function(b){return B(b)?(a=b,this):a};this.$get=["$rootScope","$browser","$sniffer","$rootElement",function(c,d,e,f){function g(a){c.$broadcast("$locationChangeSuccess",h.absUrl(), +a)}var h,m=d.baseHref(),k=d.url();if(a){if(!m)throw sb("nobase");m=k.substring(0,k.indexOf("/",k.indexOf("//")+2))+(m||"/");e=e.history?Lc:Mc}else m=Ya(k),e=Sb;h=new e(m,"#"+b);h.$$parseLinkUrl(k,k);var n=/^\s*(javascript|mailto):/i;f.on("click",function(a){if(!a.ctrlKey&&!a.metaKey&&2!=a.which){for(var b=G(a.target);"a"!==pa(b[0]);)if(b[0]===f[0]||!(b=b.parent())[0])return;var e=b.prop("href"),g=b.attr("href")||b.attr("xlink:href");S(e)&&"[object SVGAnimatedString]"===e.toString()&&(e=Aa(e.animVal).href); +n.test(e)||!e||b.attr("target")||a.isDefaultPrevented()||!h.$$parseLinkUrl(e,g)||(a.preventDefault(),h.absUrl()!=d.url()&&(c.$apply(),t.angular["ff-684208-preventDefault"]=!0))}});h.absUrl()!=k&&d.url(h.absUrl(),!0);d.onUrlChange(function(a){h.absUrl()!=a&&(c.$evalAsync(function(){var b=h.absUrl();h.$$parse(a);c.$broadcast("$locationChangeStart",a,b).defaultPrevented?(h.$$parse(b),d.url(b)):g(b)}),c.$$phase||c.$digest())});var p=0;c.$watch(function(){var a=d.url(),b=h.$$replace;p&&a==h.absUrl()|| +(p++,c.$evalAsync(function(){c.$broadcast("$locationChangeStart",h.absUrl(),a).defaultPrevented?h.$$parse(a):(d.url(h.absUrl(),b),g(a))}));h.$$replace=!1;return p});return h}]}function ze(){var b=!0,a=this;this.debugEnabled=function(a){return B(a)?(b=a,this):b};this.$get=["$window",function(c){function d(a){a instanceof Error&&(a.stack?a=a.message&&-1===a.stack.indexOf(a.message)?"Error: "+a.message+"\n"+a.stack:a.stack:a.sourceURL&&(a=a.message+"\n"+a.sourceURL+":"+a.line));return a}function e(a){var b= +c.console||{},e=b[a]||b.log||w;a=!1;try{a=!!e.apply}catch(m){}return a?function(){var a=[];r(arguments,function(b){a.push(d(b))});return e.apply(b,a)}:function(a,b){e(a,null==b?"":b)}}return{log:e("log"),info:e("info"),warn:e("warn"),error:e("error"),debug:function(){var c=e("debug");return function(){b&&c.apply(a,arguments)}}()}}]}function na(b,a){if("__defineGetter__"===b||"__defineSetter__"===b||"__lookupGetter__"===b||"__lookupSetter__"===b||"__proto__"===b)throw oa("isecfld",a);return b}function Ba(b, +a){if(b){if(b.constructor===b)throw oa("isecfn",a);if(b.window===b)throw oa("isecwindow",a);if(b.children&&(b.nodeName||b.prop&&b.attr&&b.find))throw oa("isecdom",a);if(b===Object)throw oa("isecobj",a);}return b}function ub(b,a,c,d){Ba(b,d);a=a.split(".");for(var e,f=0;1<a.length;f++){e=na(a.shift(),d);var g=Ba(b[e],d);g||(g={},b[e]=g);b=g}e=na(a.shift(),d);Ba(b[e],d);return b[e]=c}function Oc(b,a,c,d,e,f){na(b,f);na(a,f);na(c,f);na(d,f);na(e,f);return function(f,h){var m=h&&h.hasOwnProperty(b)?h: +f;if(null==m)return m;m=m[b];if(!a)return m;if(null==m)return s;m=m[a];if(!c)return m;if(null==m)return s;m=m[c];if(!d)return m;if(null==m)return s;m=m[d];return e?null==m?s:m=m[e]:m}}function Pc(b,a,c){var d=Qc[b];if(d)return d;var e=b.split("."),f=e.length;if(a.csp)d=6>f?Oc(e[0],e[1],e[2],e[3],e[4],c):function(a,b){var d=0,g;do g=Oc(e[d++],e[d++],e[d++],e[d++],e[d++],c)(a,b),b=s,a=g;while(d<f);return g};else{var g="";r(e,function(a,b){na(a,c);g+="if(s == null) return undefined;\ns="+(b?"s":'((l&&l.hasOwnProperty("'+ +a+'"))?l:s)')+"."+a+";\n"});g+="return s;";a=new Function("s","l",g);a.toString=da(g);a.assign=function(a,c){return ub(a,b,c,b)};d=a}d.sharedGetter=!0;return Qc[b]=d}function Ae(){var b=Object.create(null),a={csp:!1};this.$get=["$filter","$sniffer",function(c,d){function e(a){var b=a;a.sharedGetter&&(b=function(b,c){return a(b,c)},b.literal=a.literal,b.constant=a.constant,b.assign=a.assign);return b}function f(a,b,c,d){var e,f;return e=a.$watch(function(a){return d(a)},function(a,c,d){f=a;D(b)&&b.apply(this, +arguments);B(a)&&d.$$postDigest(function(){B(f)&&e()})},c)}function g(a,b,c,d){function e(a){var b=!0;r(a,function(a){B(a)||(b=!1)});return b}var f;return f=a.$watch(function(a){return d(a)},function(a,c,d){D(b)&&b.call(this,a,c,d);e(a)&&d.$$postDigest(function(){e(a)&&f()})},c)}function h(a,b,c,d){var e;return e=a.$watch(function(a){return d(a)},function(a,c,d){D(b)&&b.apply(this,arguments);e()},c)}function m(a,b){if(!b)return a;var c=function(c,d){var e=a(c,d),f=b(e,c,d);return B(e)?f:e};c.$$watchDelegate= +a.$$watchDelegate;return c}a.csp=d.csp;return function(d,n){var p,l,q;switch(typeof d){case "string":return q=d=d.trim(),p=b[q],p||(":"===d.charAt(0)&&":"===d.charAt(1)&&(l=!0,d=d.substring(2)),p=new Tb(a),p=(new Za(p,c,a)).parse(d),p.constant?p.$$watchDelegate=h:l&&(p=e(p),p.$$watchDelegate=p.literal?g:f),b[q]=p),m(p,n);case "function":return m(d,n);default:return m(w,n)}}}]}function Ce(){this.$get=["$rootScope","$exceptionHandler",function(b,a){return Rc(function(a){b.$evalAsync(a)},a)}]}function De(){this.$get= +["$browser","$exceptionHandler",function(b,a){return Rc(function(a){b.defer(a)},a)}]}function Rc(b,a){function c(a,b,c){function d(b){return function(c){e||(e=!0,b.call(a,c))}}var e=!1;return[d(b),d(c)]}function d(){this.$$state={status:0}}function e(a,b){return function(c){b.call(a,c)}}function f(c){!c.processScheduled&&c.pending&&(c.processScheduled=!0,b(function(){var b,d,e;e=c.pending;c.processScheduled=!1;c.pending=s;for(var f=0,h=e.length;f<h;++f){d=e[f][0];b=e[f][c.status];try{D(b)?d.resolve(b(c.value)): +1===c.status?d.resolve(c.value):d.reject(c.value)}catch(g){d.reject(g),a(g)}}}))}function g(){this.promise=new d;this.resolve=e(this,this.resolve);this.reject=e(this,this.reject);this.notify=e(this,this.notify)}var h=K("$q",TypeError);d.prototype={then:function(a,b,c){var d=new g;this.$$state.pending=this.$$state.pending||[];this.$$state.pending.push([d,a,b,c]);0<this.$$state.status&&f(this.$$state);return d.promise},"catch":function(a){return this.then(null,a)},"finally":function(a,b){return this.then(function(b){return k(b, +!0,a)},function(b){return k(b,!1,a)},b)}};g.prototype={resolve:function(a){this.promise.$$state.status||(a===this.promise?this.$$reject(h("qcycle",a)):this.$$resolve(a))},$$resolve:function(b){var d,e;e=c(this,this.$$resolve,this.$$reject);try{if(S(b)||D(b))d=b&&b.then;D(d)?(this.promise.$$state.status=-1,d.call(b,e[0],e[1],this.notify)):(this.promise.$$state.value=b,this.promise.$$state.status=1,f(this.promise.$$state))}catch(h){e[1](h),a(h)}},reject:function(a){this.promise.$$state.status||this.$$reject(a)}, +$$reject:function(a){this.promise.$$state.value=a;this.promise.$$state.status=2;f(this.promise.$$state)},notify:function(c){var d=this.promise.$$state.pending;0>=this.promise.$$state.status&&d&&d.length&&b(function(){for(var b,e,f=0,h=d.length;f<h;f++){e=d[f][0];b=d[f][3];try{e.notify(D(b)?b(c):c)}catch(g){a(g)}}})}};var m=function(a,b){var c=new g;b?c.resolve(a):c.reject(a);return c.promise},k=function(a,b,c){var d=null;try{D(c)&&(d=c())}catch(e){return m(e,!1)}return d&&D(d.then)?d.then(function(){return m(a, +b)},function(a){return m(a,!1)}):m(a,b)},n=function(a,b,c,d){var e=new g;e.resolve(a);return e.promise.then(b,c,d)},p=function q(a){if(!D(a))throw h("norslvr",a);if(!(this instanceof q))return new q(a);var b=new g;a(function(a){b.resolve(a)},function(a){b.reject(a)});return b.promise};p.defer=function(){return new g};p.reject=function(a){var b=new g;b.reject(a);return b.promise};p.when=n;p.all=function(a){var b=new g,c=0,d=L(a)?[]:{};r(a,function(a,e){c++;n(a).then(function(a){d.hasOwnProperty(e)|| +(d[e]=a,--c||b.resolve(d))},function(a){d.hasOwnProperty(e)||b.reject(a)})});0===c&&b.resolve(d);return b.promise};return p}function Me(){this.$get=["$window","$timeout",function(b,a){var c=b.requestAnimationFrame||b.webkitRequestAnimationFrame||b.mozRequestAnimationFrame,d=b.cancelAnimationFrame||b.webkitCancelAnimationFrame||b.mozCancelAnimationFrame||b.webkitCancelRequestAnimationFrame,e=!!c,f=e?function(a){var b=c(a);return function(){d(b)}}:function(b){var c=a(b,16.66,!1);return function(){a.cancel(c)}}; +f.supported=e;return f}]}function Be(){var b=10,a=K("$rootScope"),c=null,d=null;this.digestTtl=function(a){arguments.length&&(b=a);return b};this.$get=["$injector","$exceptionHandler","$parse","$browser",function(e,f,g,h){function m(){this.$id=++bb;this.$$phase=this.$parent=this.$$watchers=this.$$nextSibling=this.$$prevSibling=this.$$childHead=this.$$childTail=null;this["this"]=this.$root=this;this.$$destroyed=!1;this.$$asyncQueue=[];this.$$postDigestQueue=[];this.$$listeners={};this.$$listenerCount= +{};this.$$isolateBindings={};this.$$applyAsyncQueue=[]}function k(b){if(A.$$phase)throw a("inprog",A.$$phase);A.$$phase=b}function n(a,b,c){do a.$$listenerCount[c]-=b,0===a.$$listenerCount[c]&&delete a.$$listenerCount[c];while(a=a.$parent)}function p(){}function l(){for(var a=A.$$applyAsyncQueue;a.length;)try{a.shift()()}catch(b){f(b)}d=null}function q(){null===d&&(d=h.defer(function(){A.$apply(l)}))}m.prototype={constructor:m,$new:function(a){a?(a=new m,a.$root=this.$root,a.$$asyncQueue=this.$$asyncQueue, +a.$$postDigestQueue=this.$$postDigestQueue):(this.$$ChildScope||(this.$$ChildScope=function(){this.$$watchers=this.$$nextSibling=this.$$childHead=this.$$childTail=null;this.$$listeners={};this.$$listenerCount={};this.$id=++bb;this.$$ChildScope=null},this.$$ChildScope.prototype=this),a=new this.$$ChildScope);a["this"]=a;a.$parent=this;a.$$prevSibling=this.$$childTail;this.$$childHead?this.$$childTail=this.$$childTail.$$nextSibling=a:this.$$childHead=this.$$childTail=a;return a},$watch:function(a,b, +d){var e=g(a);if(e.$$watchDelegate)return e.$$watchDelegate(this,b,d,e);var f=this.$$watchers,h={fn:b,last:p,get:e,exp:a,eq:!!d};c=null;D(b)||(h.fn=w);f||(f=this.$$watchers=[]);f.unshift(h);return function(){Ra(f,h);c=null}},$watchGroup:function(a,b){function c(){g=!1;k?(k=!1,b(e,e,h)):b(e,d,h)}var d=Array(a.length),e=Array(a.length),f=[],h=this,g=!1,k=!0;if(!a.length){var m=!0;h.$evalAsync(function(){m&&b(e,e,h)});return function(){m=!1}}if(1===a.length)return this.$watch(a[0],function(a,c,f){e[0]= +a;d[0]=c;b(e,a===c?e:d,f)});r(a,function(a,b){var k=h.$watch(a,function(a,f){e[b]=a;d[b]=f;g||(g=!0,h.$evalAsync(c))});f.push(k)});return function(){for(;f.length;)f.shift()()}},$watchCollection:function(a,b){var c=this,d,e,f,h=1<b.length,k=0,m=g(a,function(a){d=a;var b,c,f,h;if(S(d))if(Na(d))for(e!==l&&(e=l,q=e.length=0,k++),a=d.length,q!==a&&(k++,e.length=q=a),b=0;b<a;b++)h=e[b],f=d[b],c=h!==h&&f!==f,c||h===f||(k++,e[b]=f);else{e!==n&&(e=n={},q=0,k++);a=0;for(b in d)d.hasOwnProperty(b)&&(a++,f= +d[b],h=e[b],b in e?(c=h!==h&&f!==f,c||h===f||(k++,e[b]=f)):(q++,e[b]=f,k++));if(q>a)for(b in k++,e)d.hasOwnProperty(b)||(q--,delete e[b])}else e!==d&&(e=d,k++);return k}),l=[],n={},p=!0,q=0;return this.$watch(m,function(){p?(p=!1,b(d,d,c)):b(d,f,c);if(h)if(S(d))if(Na(d)){f=Array(d.length);for(var a=0;a<d.length;a++)f[a]=d[a]}else for(a in f={},d)Ab.call(d,a)&&(f[a]=d[a]);else f=d})},$digest:function(){var e,g,m,n,q=this.$$asyncQueue,r=this.$$postDigestQueue,s,B,J=b,N,M=[],O,H,W;k("$digest");h.$$checkUrlChange(); +this===A&&null!==d&&(h.defer.cancel(d),l());c=null;do{B=!1;for(N=this;q.length;){try{W=q.shift(),W.scope.$eval(W.expression)}catch(R){f(R)}c=null}a:do{if(n=N.$$watchers)for(s=n.length;s--;)try{if(e=n[s])if((g=e.get(N))!==(m=e.last)&&!(e.eq?ra(g,m):"number"===typeof g&&"number"===typeof m&&isNaN(g)&&isNaN(m)))B=!0,c=e,e.last=e.eq?Ha(g,null):g,e.fn(g,m===p?g:m,N),5>J&&(O=4-J,M[O]||(M[O]=[]),H=D(e.exp)?"fn: "+(e.exp.name||e.exp.toString()):e.exp,H+="; newVal: "+sa(g)+"; oldVal: "+sa(m),M[O].push(H)); +else if(e===c){B=!1;break a}}catch(t){f(t)}if(!(n=N.$$childHead||N!==this&&N.$$nextSibling))for(;N!==this&&!(n=N.$$nextSibling);)N=N.$parent}while(N=n);if((B||q.length)&&!J--)throw A.$$phase=null,a("infdig",b,sa(M));}while(B||q.length);for(A.$$phase=null;r.length;)try{r.shift()()}catch(G){f(G)}},$destroy:function(){if(!this.$$destroyed){var a=this.$parent;this.$broadcast("$destroy");this.$$destroyed=!0;if(this!==A){for(var b in this.$$listenerCount)n(this,this.$$listenerCount[b],b);a.$$childHead== +this&&(a.$$childHead=this.$$nextSibling);a.$$childTail==this&&(a.$$childTail=this.$$prevSibling);this.$$prevSibling&&(this.$$prevSibling.$$nextSibling=this.$$nextSibling);this.$$nextSibling&&(this.$$nextSibling.$$prevSibling=this.$$prevSibling);this.$parent=this.$$nextSibling=this.$$prevSibling=this.$$childHead=this.$$childTail=this.$root=null;this.$$listeners={};this.$$watchers=this.$$asyncQueue=this.$$postDigestQueue=[];this.$destroy=this.$digest=this.$apply=w;this.$on=this.$watch=this.$watchGroup= +function(){return w}}}},$eval:function(a,b){return g(a)(this,b)},$evalAsync:function(a){A.$$phase||A.$$asyncQueue.length||h.defer(function(){A.$$asyncQueue.length&&A.$digest()});this.$$asyncQueue.push({scope:this,expression:a})},$$postDigest:function(a){this.$$postDigestQueue.push(a)},$apply:function(a){try{return k("$apply"),this.$eval(a)}catch(b){f(b)}finally{A.$$phase=null;try{A.$digest()}catch(c){throw f(c),c;}}},$applyAsync:function(a){function b(){c.$eval(a)}var c=this;a&&A.$$applyAsyncQueue.push(b); +q()},$on:function(a,b){var c=this.$$listeners[a];c||(this.$$listeners[a]=c=[]);c.push(b);var d=this;do d.$$listenerCount[a]||(d.$$listenerCount[a]=0),d.$$listenerCount[a]++;while(d=d.$parent);var e=this;return function(){c[c.indexOf(b)]=null;n(e,1,a)}},$emit:function(a,b){var c=[],d,e=this,h=!1,g={name:a,targetScope:e,stopPropagation:function(){h=!0},preventDefault:function(){g.defaultPrevented=!0},defaultPrevented:!1},k=db([g],arguments,1),m,l;do{d=e.$$listeners[a]||c;g.currentScope=e;m=0;for(l= +d.length;m<l;m++)if(d[m])try{d[m].apply(null,k)}catch(n){f(n)}else d.splice(m,1),m--,l--;if(h)return g.currentScope=null,g;e=e.$parent}while(e);g.currentScope=null;return g},$broadcast:function(a,b){var c=this,d=this,e={name:a,targetScope:this,preventDefault:function(){e.defaultPrevented=!0},defaultPrevented:!1};if(!this.$$listenerCount[a])return e;for(var h=db([e],arguments,1),g,k;c=d;){e.currentScope=c;d=c.$$listeners[a]||[];g=0;for(k=d.length;g<k;g++)if(d[g])try{d[g].apply(null,h)}catch(m){f(m)}else d.splice(g, +1),g--,k--;if(!(d=c.$$listenerCount[a]&&c.$$childHead||c!==this&&c.$$nextSibling))for(;c!==this&&!(d=c.$$nextSibling);)c=c.$parent}e.currentScope=null;return e}};var A=new m;return A}]}function Ed(){var b=/^\s*(https?|ftp|mailto|tel|file):/,a=/^\s*((https?|ftp|file|blob):|data:image\/)/;this.aHrefSanitizationWhitelist=function(a){return B(a)?(b=a,this):b};this.imgSrcSanitizationWhitelist=function(b){return B(b)?(a=b,this):a};this.$get=function(){return function(c,d){var e=d?a:b,f;if(!X||8<=X)if(f= +Aa(c).href,""!==f&&!f.match(e))return"unsafe:"+f;return c}}}function ef(b){if("self"===b)return b;if(C(b)){if(-1<b.indexOf("***"))throw Ca("iwcard",b);b=b.replace(/([-()\[\]{}+?*.$\^|,:#<!\\])/g,"\\$1").replace(/\x08/g,"\\x08").replace("\\*\\*",".*").replace("\\*","[^:/.?&;]*");return new RegExp("^"+b+"$")}if(cb(b))return new RegExp("^"+b.source+"$");throw Ca("imatcher");}function Sc(b){var a=[];B(b)&&r(b,function(b){a.push(ef(b))});return a}function Fe(){this.SCE_CONTEXTS=ka;var b=["self"],a=[]; +this.resourceUrlWhitelist=function(a){arguments.length&&(b=Sc(a));return b};this.resourceUrlBlacklist=function(b){arguments.length&&(a=Sc(b));return a};this.$get=["$injector",function(c){function d(a,b){return"self"===a?Ic(b):!!a.exec(b.href)}function e(a){var b=function(a){this.$$unwrapTrustedValue=function(){return a}};a&&(b.prototype=new a);b.prototype.valueOf=function(){return this.$$unwrapTrustedValue()};b.prototype.toString=function(){return this.$$unwrapTrustedValue().toString()};return b} +var f=function(a){throw Ca("unsafe");};c.has("$sanitize")&&(f=c.get("$sanitize"));var g=e(),h={};h[ka.HTML]=e(g);h[ka.CSS]=e(g);h[ka.URL]=e(g);h[ka.JS]=e(g);h[ka.RESOURCE_URL]=e(h[ka.URL]);return{trustAs:function(a,b){var c=h.hasOwnProperty(a)?h[a]:null;if(!c)throw Ca("icontext",a,b);if(null===b||b===s||""===b)return b;if("string"!==typeof b)throw Ca("itype",a);return new c(b)},getTrusted:function(c,e){if(null===e||e===s||""===e)return e;var g=h.hasOwnProperty(c)?h[c]:null;if(g&&e instanceof g)return e.$$unwrapTrustedValue(); +if(c===ka.RESOURCE_URL){var g=Aa(e.toString()),p,l,q=!1;p=0;for(l=b.length;p<l;p++)if(d(b[p],g)){q=!0;break}if(q)for(p=0,l=a.length;p<l;p++)if(d(a[p],g)){q=!1;break}if(q)return e;throw Ca("insecurl",e.toString());}if(c===ka.HTML)return f(e);throw Ca("unsafe");},valueOf:function(a){return a instanceof g?a.$$unwrapTrustedValue():a}}}]}function Ee(){var b=!0;this.enabled=function(a){arguments.length&&(b=!!a);return b};this.$get=["$parse","$sniffer","$sceDelegate",function(a,c,d){if(b&&c.msie&&8>c.msieDocumentMode)throw Ca("iequirks"); +var e=qa(ka);e.isEnabled=function(){return b};e.trustAs=d.trustAs;e.getTrusted=d.getTrusted;e.valueOf=d.valueOf;b||(e.trustAs=e.getTrusted=function(a,b){return b},e.valueOf=Pa);e.parseAs=function(b,c){var d=a(c);return d.literal&&d.constant?d:a(c,function(a){return e.getTrusted(b,a)})};var f=e.parseAs,g=e.getTrusted,h=e.trustAs;r(ka,function(a,b){var c=P(b);e[Wa("parse_as_"+c)]=function(b){return f(a,b)};e[Wa("get_trusted_"+c)]=function(b){return g(a,b)};e[Wa("trust_as_"+c)]=function(b){return h(a, +b)}});return e}]}function Ge(){this.$get=["$window","$document",function(b,a){var c={},d=U((/android (\d+)/.exec(P((b.navigator||{}).userAgent))||[])[1]),e=/Boxee/i.test((b.navigator||{}).userAgent),f=a[0]||{},g=f.documentMode,h,m=/^(Moz|webkit|O|ms)(?=[A-Z])/,k=f.body&&f.body.style,n=!1,p=!1;if(k){for(var l in k)if(n=m.exec(l)){h=n[0];h=h.substr(0,1).toUpperCase()+h.substr(1);break}h||(h="WebkitOpacity"in k&&"webkit");n=!!("transition"in k||h+"Transition"in k);p=!!("animation"in k||h+"Animation"in +k);!d||n&&p||(n=C(f.body.style.webkitTransition),p=C(f.body.style.webkitAnimation))}return{history:!(!b.history||!b.history.pushState||4>d||e),hashchange:"onhashchange"in b&&(!g||7<g),hasEvent:function(a){if("input"==a&&9==X)return!1;if(F(c[a])){var b=f.createElement("div");c[a]="on"+a in b}return c[a]},csp:Ua(),vendorPrefix:h,transitions:n,animations:p,android:d,msie:X,msieDocumentMode:g}}]}function Ie(){this.$get=["$templateCache","$http","$q",function(b,a,c){function d(e,f){function g(){h.totalPendingRequests--; +if(!f)throw ja("tpload",e);return c.reject()}var h=d;h.totalPendingRequests++;return a.get(e,{cache:b}).then(function(a){a=a.data;if(!a||0===a.length)return g();h.totalPendingRequests--;b.put(e,a);return a},g)}d.totalPendingRequests=0;return d}]}function Je(){this.$get=["$rootScope","$browser","$location",function(b,a,c){return{findBindings:function(a,b,c){a=a.getElementsByClassName("ng-binding");var g=[];r(a,function(a){var d=Ea.element(a).data("$binding");d&&r(d,function(d){c?(new RegExp("(^|\\s)"+ +b+"(\\s|\\||$)")).test(d)&&g.push(a):-1!=d.indexOf(b)&&g.push(a)})});return g},findModels:function(a,b,c){for(var g=["ng-","data-ng-","ng\\:"],h=0;h<g.length;++h){var m=a.querySelectorAll("["+g[h]+"model"+(c?"=":"*=")+'"'+b+'"]');if(m.length)return m}},getLocation:function(){return c.url()},setLocation:function(a){a!==c.url()&&(c.url(a),b.$digest())},whenStable:function(b){a.notifyWhenNoOutstandingRequests(b)}}}]}function Ke(){this.$get=["$rootScope","$browser","$q","$$q","$exceptionHandler",function(b, +a,c,d,e){function f(f,m,k){var n=B(k)&&!k,p=(n?d:c).defer(),l=p.promise;m=a.defer(function(){try{p.resolve(f())}catch(a){p.reject(a),e(a)}finally{delete g[l.$$timeoutId]}n||b.$apply()},m);l.$$timeoutId=m;g[m]=p;return l}var g={};f.cancel=function(b){return b&&b.$$timeoutId in g?(g[b.$$timeoutId].reject("canceled"),delete g[b.$$timeoutId],a.defer.cancel(b.$$timeoutId)):!1};return f}]}function Aa(b,a){var c=b;X&&(Z.setAttribute("href",c),c=Z.href);Z.setAttribute("href",c);return{href:Z.href,protocol:Z.protocol? +Z.protocol.replace(/:$/,""):"",host:Z.host,search:Z.search?Z.search.replace(/^\?/,""):"",hash:Z.hash?Z.hash.replace(/^#/,""):"",hostname:Z.hostname,port:Z.port,pathname:"/"===Z.pathname.charAt(0)?Z.pathname:"/"+Z.pathname}}function Ic(b){b=C(b)?Aa(b):b;return b.protocol===Tc.protocol&&b.host===Tc.host}function Le(){this.$get=da(t)}function qc(b){function a(c,d){if(S(c)){var e={};r(c,function(b,c){e[c]=a(c,b)});return e}return b.factory(c+"Filter",d)}this.register=a;this.$get=["$injector",function(a){return function(b){return a.get(b+ +"Filter")}}];a("currency",Uc);a("date",Vc);a("filter",ff);a("json",gf);a("limitTo",hf);a("lowercase",jf);a("number",Wc);a("orderBy",Xc);a("uppercase",kf)}function ff(){return function(b,a,c){if(!L(b))return b;var d=typeof c,e=[];e.check=function(a,b){for(var c=0;c<e.length;c++)if(!e[c](a,b))return!1;return!0};"function"!==d&&(c="boolean"===d&&c?function(a,b){return Ea.equals(a,b)}:function(a,b){if(a&&b&&"object"===typeof a&&"object"===typeof b){for(var d in a)if("$"!==d.charAt(0)&&Ab.call(a,d)&&c(a[d], +b[d]))return!0;return!1}b=(""+b).toLowerCase();return-1<(""+a).toLowerCase().indexOf(b)});var f=function(a,b){if("string"==typeof b&&"!"===b.charAt(0))return!f(a,b.substr(1));switch(typeof a){case "boolean":case "number":case "string":return c(a,b);case "object":switch(typeof b){case "object":return c(a,b);default:for(var d in a)if("$"!==d.charAt(0)&&f(a[d],b))return!0}return!1;case "array":for(d=0;d<a.length;d++)if(f(a[d],b))return!0;return!1;default:return!1}};switch(typeof a){case "boolean":case "number":case "string":a= +{$:a};case "object":for(var g in a)(function(b){"undefined"!==typeof a[b]&&e.push(function(c){return f("$"==b?c:c&&c[b],a[b])})})(g);break;case "function":e.push(a);break;default:return b}d=[];for(g=0;g<b.length;g++){var h=b[g];e.check(h,g)&&d.push(h)}return d}}function Uc(b){var a=b.NUMBER_FORMATS;return function(b,d){F(d)&&(d=a.CURRENCY_SYM);return null==b?b:Yc(b,a.PATTERNS[1],a.GROUP_SEP,a.DECIMAL_SEP,2).replace(/\u00A4/g,d)}}function Wc(b){var a=b.NUMBER_FORMATS;return function(b,d){return null== +b?b:Yc(b,a.PATTERNS[0],a.GROUP_SEP,a.DECIMAL_SEP,d)}}function Yc(b,a,c,d,e){if(!isFinite(b)||S(b))return"";var f=0>b;b=Math.abs(b);var g=b+"",h="",m=[],k=!1;if(-1!==g.indexOf("e")){var n=g.match(/([\d\.]+)e(-?)(\d+)/);n&&"-"==n[2]&&n[3]>e+1?(g="0",b=0):(h=g,k=!0)}if(k)0<e&&-1<b&&1>b&&(h=b.toFixed(e));else{g=(g.split(Zc)[1]||"").length;F(e)&&(e=Math.min(Math.max(a.minFrac,g),a.maxFrac));b=+(Math.round(+(b.toString()+"e"+e)).toString()+"e"+-e);0===b&&(f=!1);b=(""+b).split(Zc);g=b[0];b=b[1]||"";var n= +0,p=a.lgSize,l=a.gSize;if(g.length>=p+l)for(n=g.length-p,k=0;k<n;k++)0===(n-k)%l&&0!==k&&(h+=c),h+=g.charAt(k);for(k=n;k<g.length;k++)0===(g.length-k)%p&&0!==k&&(h+=c),h+=g.charAt(k);for(;b.length<e;)b+="0";e&&"0"!==e&&(h+=d+b.substr(0,e))}m.push(f?a.negPre:a.posPre);m.push(h);m.push(f?a.negSuf:a.posSuf);return m.join("")}function vb(b,a,c){var d="";0>b&&(d="-",b=-b);for(b=""+b;b.length<a;)b="0"+b;c&&(b=b.substr(b.length-a));return d+b}function $(b,a,c,d){c=c||0;return function(e){e=e["get"+b](); +if(0<c||e>-c)e+=c;0===e&&-12==c&&(e=12);return vb(e,a,d)}}function wb(b,a){return function(c,d){var e=c["get"+b](),f=ib(a?"SHORT"+b:b);return d[f][e]}}function $c(b){var a=(new Date(b,0,1)).getDay();return new Date(b,0,(4>=a?5:12)-a)}function ad(b){return function(a){var c=$c(a.getFullYear());a=+new Date(a.getFullYear(),a.getMonth(),a.getDate()+(4-a.getDay()))-+c;a=1+Math.round(a/6048E5);return vb(a,b)}}function Vc(b){function a(a){var b;if(b=a.match(c)){a=new Date(0);var f=0,g=0,h=b[8]?a.setUTCFullYear: +a.setFullYear,m=b[8]?a.setUTCHours:a.setHours;b[9]&&(f=U(b[9]+b[10]),g=U(b[9]+b[11]));h.call(a,U(b[1]),U(b[2])-1,U(b[3]));f=U(b[4]||0)-f;g=U(b[5]||0)-g;h=U(b[6]||0);b=Math.round(1E3*parseFloat("0."+(b[7]||0)));m.call(a,f,g,h,b)}return a}var c=/^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(Z|([+-])(\d\d):?(\d\d))?)?$/;return function(c,e,f){var g="",h=[],m,k;e=e||"mediumDate";e=b.DATETIME_FORMATS[e]||e;C(c)&&(c=lf.test(c)?U(c):a(c));ea(c)&&(c=new Date(c));if(!fa(c))return c; +for(;e;)(k=mf.exec(e))?(h=db(h,k,1),e=h.pop()):(h.push(e),e=null);f&&"UTC"===f&&(c=new Date(c.getTime()),c.setMinutes(c.getMinutes()+c.getTimezoneOffset()));r(h,function(a){m=nf[a];g+=m?m(c,b.DATETIME_FORMATS):a.replace(/(^'|'$)/g,"").replace(/''/g,"'")});return g}}function gf(){return function(b){return sa(b,!0)}}function hf(){return function(b,a){if(!L(b)&&!C(b))return b;a=Infinity===Math.abs(Number(a))?Number(a):U(a);if(C(b))return a?0<=a?b.slice(0,a):b.slice(a,b.length):"";var c=[],d,e;a>b.length? +a=b.length:a<-b.length&&(a=-b.length);0<a?(d=0,e=a):(d=b.length+a,e=b.length);for(;d<e;d++)c.push(b[d]);return c}}function Xc(b){return function(a,c,d){function e(a,b){return b?function(b,c){return a(c,b)}:a}function f(a,b){var c=typeof a,d=typeof b;return c==d?(fa(a)&&fa(b)&&(a=a.valueOf(),b=b.valueOf()),"string"==c&&(a=a.toLowerCase(),b=b.toLowerCase()),a===b?0:a<b?-1:1):c<d?-1:1}if(!Na(a)||!c)return a;c=L(c)?c:[c];c=sd(c,function(a){var c=!1,d=a||Pa;if(C(a)){if("+"==a.charAt(0)||"-"==a.charAt(0))c= +"-"==a.charAt(0),a=a.substring(1);d=b(a);if(d.constant){var h=d();return e(function(a,b){return f(a[h],b[h])},c)}}return e(function(a,b){return f(d(a),d(b))},c)});for(var g=[],h=0;h<a.length;h++)g.push(a[h]);return g.sort(e(function(a,b){for(var d=0;d<c.length;d++){var e=c[d](a,b);if(0!==e)return e}return 0},d))}}function Fa(b){D(b)&&(b={link:b});b.restrict=b.restrict||"AC";return da(b)}function bd(b,a,c,d){var e=this,f=b.parent().controller("form")||xb,g=[];e.$error={};e.$$success={};e.$pending= +s;e.$name=a.name||a.ngForm;e.$dirty=!1;e.$pristine=!0;e.$valid=!0;e.$invalid=!1;e.$submitted=!1;f.$addControl(e);b.addClass(Ma);e.$rollbackViewValue=function(){r(g,function(a){a.$rollbackViewValue()})};e.$commitViewValue=function(){r(g,function(a){a.$commitViewValue()})};e.$addControl=function(a){Ja(a.$name,"input");g.push(a);a.$name&&(e[a.$name]=a)};e.$removeControl=function(a){a.$name&&e[a.$name]===a&&delete e[a.$name];r(e.$pending,function(b,c){e.$setValidity(c,null,a)});r(e.$error,function(b, +c){e.$setValidity(c,null,a)});Ra(g,a)};cd({ctrl:this,$element:b,set:function(a,b,c){var d=a[b];d?-1===d.indexOf(c)&&d.push(c):a[b]=[c]},unset:function(a,b,c){var d=a[b];d&&(Ra(d,c),0===d.length&&delete a[b])},parentForm:f,$animate:d});e.$setDirty=function(){d.removeClass(b,Ma);d.addClass(b,yb);e.$dirty=!0;e.$pristine=!1;f.$setDirty()};e.$setPristine=function(){d.setClass(b,Ma,yb+" ng-submitted");e.$dirty=!1;e.$pristine=!0;e.$submitted=!1;r(g,function(a){a.$setPristine()})};e.$setSubmitted=function(){d.addClass(b, +"ng-submitted");e.$submitted=!0;f.$setSubmitted()}}function Ub(b){b.$formatters.push(function(a){return b.$isEmpty(a)?a:a.toString()})}function $a(b,a,c,d,e,f){a.prop("validity");var g=a[0].placeholder,h={},m=P(a[0].type);if(!e.android){var k=!1;a.on("compositionstart",function(a){k=!0});a.on("compositionend",function(){k=!1;n()})}var n=function(b){if(!k){var e=a.val(),f=b&&b.type;X&&"input"===(b||h).type&&a[0].placeholder!==g?g=a[0].placeholder:("password"===m||c.ngTrim&&"false"===c.ngTrim||(e=ba(e)), +(d.$viewValue!==e||""===e&&d.$$hasNativeValidators)&&d.$setViewValue(e,f))}};if(e.hasEvent("input"))a.on("input",n);else{var p,l=function(a){p||(p=f.defer(function(){n(a);p=null}))};a.on("keydown",function(a){var b=a.keyCode;91===b||15<b&&19>b||37<=b&&40>=b||l(a)});if(e.hasEvent("paste"))a.on("paste cut",l)}a.on("change",n);d.$render=function(){a.val(d.$isEmpty(d.$viewValue)?"":d.$viewValue)}}function zb(b,a){return function(c){var d;if(fa(c))return c;if(C(c)){'"'==c.charAt(0)&&'"'==c.charAt(c.length- +1)&&(c=c.substring(1,c.length-1));if(of.test(c))return new Date(c);b.lastIndex=0;if(c=b.exec(c))return c.shift(),d={yyyy:1970,MM:1,dd:1,HH:0,mm:0,ss:0},r(c,function(b,c){c<a.length&&(d[a[c]]=+b)}),new Date(d.yyyy,d.MM-1,d.dd,d.HH,d.mm,d.ss||0)}return NaN}}function ab(b,a,c,d){return function(e,f,g,h,m,k,n){function p(a){return B(a)?fa(a)?a:c(a):s}dd(e,f,g,h);$a(e,f,g,h,m,k);var l=h&&h.$options&&h.$options.timezone;h.$$parserName=b;h.$parsers.push(function(b){return h.$isEmpty(b)?null:a.test(b)?(b= +c(b),"UTC"===l&&b.setMinutes(b.getMinutes()-b.getTimezoneOffset()),b):s});h.$formatters.push(function(a){return fa(a)?n("date")(a,d,l):""});if(B(g.min)||g.ngMin){var q;h.$validators.min=function(a){return h.$isEmpty(a)||F(q)||c(a)>=q};g.$observe("min",function(a){q=p(a);h.$validate()})}if(B(g.max)||g.ngMax){var r;h.$validators.max=function(a){return h.$isEmpty(a)||F(r)||c(a)<=r};g.$observe("max",function(a){r=p(a);h.$validate()})}}}function dd(b,a,c,d){(d.$$hasNativeValidators=S(a[0].validity))&& +d.$parsers.push(function(b){var c=a.prop("validity")||{};return c.badInput&&!c.typeMismatch?s:b})}function ed(b,a,c,d,e){if(B(d)){b=b(d);if(!b.constant)throw K("ngModel")("constexpr",c,d);return b(a)}return e}function cd(b){function a(a,b){b&&!f[a]?(k.addClass(e,a),f[a]=!0):!b&&f[a]&&(k.removeClass(e,a),f[a]=!1)}function c(b,c){b=b?"-"+Db(b,"-"):"";a(pf+b,!0===c);a(qf+b,!1===c)}var d=b.ctrl,e=b.$element,f={},g=b.set,h=b.unset,m=b.parentForm,k=b.$animate;d.$setValidity=function(b,e,f){e===s?(d.$pending|| +(d.$pending={}),g(d.$pending,b,f)):(d.$pending&&h(d.$pending,b,f),fd(d.$pending)&&(d.$pending=s));"boolean"!==typeof e?(h(d.$error,b,f),h(d.$$success,b,f)):e?(h(d.$error,b,f),g(d.$$success,b,f)):(g(d.$error,b,f),h(d.$$success,b,f));d.$pending?(a(gd,!0),d.$valid=d.$invalid=s,c("",null)):(a(gd,!1),d.$valid=fd(d.$error),d.$invalid=!d.$valid,c("",d.$valid));e=d.$pending&&d.$pending[b]?s:d.$error[b]?!1:d.$$success[b]?!0:null;c(b,e);m.$setValidity(b,e,d)};c("",!0)}function fd(b){if(b)for(var a in b)return!1; +return!0}function Vb(b,a){b="ngClass"+b;return["$animate",function(c){function d(a,b){var c=[],d=0;a:for(;d<a.length;d++){for(var e=a[d],n=0;n<b.length;n++)if(e==b[n])continue a;c.push(e)}return c}function e(a){if(!L(a)){if(C(a))return a.split(" ");if(S(a)){var b=[];r(a,function(a,c){a&&(b=b.concat(c.split(" ")))});return b}}return a}return{restrict:"AC",link:function(f,g,h){function m(a,b){var c=g.data("$classCounts")||{},d=[];r(a,function(a){if(0<b||c[a])c[a]=(c[a]||0)+b,c[a]===+(0<b)&&d.push(a)}); +g.data("$classCounts",c);return d.join(" ")}function k(b){if(!0===a||f.$index%2===a){var k=e(b||[]);if(!n){var q=m(k,1);h.$addClass(q)}else if(!ra(b,n)){var r=e(n),q=d(k,r),k=d(r,k),q=m(q,1),k=m(k,-1);q&&q.length&&c.addClass(g,q);k&&k.length&&c.removeClass(g,k)}}n=qa(b)}var n;f.$watch(h[b],k,!0);h.$observe("class",function(a){k(f.$eval(h[b]))});"ngClass"!==b&&f.$watch("$index",function(c,d){var g=c&1;if(g!==(d&1)){var k=e(f.$eval(h[b]));g===a?(g=m(k,1),h.$addClass(g)):(g=m(k,-1),h.$removeClass(g))}})}}}]} +var rf=/^\/(.+)\/([a-z]*)$/,P=function(b){return C(b)?b.toLowerCase():b},Ab=Object.prototype.hasOwnProperty,ib=function(b){return C(b)?b.toUpperCase():b},X,G,la,Ta=[].slice,sf=[].push,Ga=Object.prototype.toString,Sa=K("ng"),Ea=t.angular||(t.angular={}),Va,bb=0;X=U((/msie (\d+)/.exec(P(navigator.userAgent))||[])[1]);isNaN(X)&&(X=U((/trident\/.*; rv:(\d+)/.exec(P(navigator.userAgent))||[])[1]));w.$inject=[];Pa.$inject=[];var L=Array.isArray,ba=function(b){return C(b)?b.trim():b},Ua=function(){if(B(Ua.isActive_))return Ua.isActive_; +var b=!(!Y.querySelector("[ng-csp]")&&!Y.querySelector("[data-ng-csp]"));if(!b)try{new Function("")}catch(a){b=!0}return Ua.isActive_=b},fb=["ng-","data-ng-","ng:","x-ng-"],yd=/[A-Z]/g,hc=!1,Eb,Cd={full:"1.3.0-rc.1",major:1,minor:3,dot:0,codeName:"backyard-atomicity"};V.expando="ng339";var ob=V.cache={},Te=1;V._data=function(b){return this.cache[b[this.expando]]||{}};var Oe=/([\:\-\_]+(.))/g,Pe=/^moz([A-Z])/,tf={mouseleave:"mouseout",mouseenter:"mouseover"},Hb=K("jqLite"),Se=/^<(\w+)\s*\/?>(?:<\/\1>|)$/, +Gb=/<|&#?\w+;/,Qe=/<([\w:]+)/,Re=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,ia={option:[1,'<select multiple="multiple">',"</select>"],thead:[1,"<table>","</table>"],col:[2,"<table><colgroup>","</colgroup></table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:[0,"",""]};ia.optgroup=ia.option;ia.tbody=ia.tfoot=ia.colgroup=ia.caption=ia.thead;ia.th=ia.td;var Ia=V.prototype={ready:function(b){function a(){c||(c= +!0,b())}var c=!1;"complete"===Y.readyState?setTimeout(a):(this.on("DOMContentLoaded",a),V(t).on("load",a),this.on("DOMContentLoaded",a))},toString:function(){var b=[];r(this,function(a){b.push(""+a)});return"["+b.join(", ")+"]"},eq:function(b){return 0<=b?G(this[b]):G(this[this.length+b])},length:0,push:sf,sort:[].sort,splice:[].splice},qb={};r("multiple selected checked disabled readOnly required open".split(" "),function(b){qb[P(b)]=b});var zc={};r("input select option textarea button form details".split(" "), +function(b){zc[b]=!0});var Ac={ngMinlength:"minlength",ngMaxlength:"maxlength",ngMin:"min",ngMax:"max",ngPattern:"pattern"};r({data:Jb,removeData:mb},function(b,a){V[a]=b});r({data:Jb,inheritedData:pb,scope:function(b){return G.data(b,"$scope")||pb(b.parentNode||b,["$isolateScope","$scope"])},isolateScope:function(b){return G.data(b,"$isolateScope")||G.data(b,"$isolateScopeNoTemplate")},controller:vc,injector:function(b){return pb(b,"$injector")},removeAttr:function(b,a){b.removeAttribute(a)},hasClass:jb, +css:function(b,a,c){a=Wa(a);if(B(c))b.style[a]=c;else return b.style[a]},attr:function(b,a,c){var d=P(a);if(qb[d])if(B(c))c?(b[a]=!0,b.setAttribute(a,d)):(b[a]=!1,b.removeAttribute(d));else return b[a]||(b.attributes.getNamedItem(a)||w).specified?d:s;else if(B(c))b.setAttribute(a,c);else if(b.getAttribute)return b=b.getAttribute(a,2),null===b?s:b},prop:function(b,a,c){if(B(c))b[a]=c;else return b[a]},text:function(){function b(a,b){if(F(b)){var d=a.nodeType;return 1===d||3===d?a.textContent:""}a.textContent= +b}b.$dv="";return b}(),val:function(b,a){if(F(a)){if(b.multiple&&"select"===pa(b)){var c=[];r(b.options,function(a){a.selected&&c.push(a.value||a.text)});return 0===c.length?null:c}return b.value}b.value=a},html:function(b,a){if(F(a))return b.innerHTML;lb(b,!0);b.innerHTML=a},empty:wc},function(b,a){V.prototype[a]=function(a,d){var e,f,g=this.length;if(b!==wc&&(2==b.length&&b!==jb&&b!==vc?a:d)===s){if(S(a)){for(e=0;e<g;e++)if(b===Jb)b(this[e],a);else for(f in a)b(this[e],f,a[f]);return this}e=b.$dv; +g=e===s?Math.min(g,1):g;for(f=0;f<g;f++){var h=b(this[f],a,d);e=e?e+h:h}return e}for(e=0;e<g;e++)b(this[e],a,d);return this}});r({removeData:mb,on:function a(c,d,e,f){if(B(f))throw Hb("onargs");if(rc(c)){var g=nb(c,!0);f=g.events;var h=g.handle;h||(h=g.handle=Ve(c,f));for(var g=0<=d.indexOf(" ")?d.split(" "):[d],m=g.length;m--;){d=g[m];var k=f[d];k||(f[d]=[],"mouseenter"===d||"mouseleave"===d?a(c,tf[d],function(a){var c=a.relatedTarget;c&&(c===this||this.contains(c))||h(a,d)}):"$destroy"!==d&&c.addEventListener(d, +h,!1),k=f[d]);k.push(e)}}},off:uc,one:function(a,c,d){a=G(a);a.on(c,function f(){a.off(c,d);a.off(c,f)});a.on(c,d)},replaceWith:function(a,c){var d,e=a.parentNode;lb(a);r(new V(c),function(c){d?e.insertBefore(c,d.nextSibling):e.replaceChild(c,a);d=c})},children:function(a){var c=[];r(a.childNodes,function(a){1===a.nodeType&&c.push(a)});return c},contents:function(a){return a.contentDocument||a.childNodes||[]},append:function(a,c){var d=a.nodeType;if(1===d||11===d){c=new V(c);for(var d=0,e=c.length;d< +e;d++)a.appendChild(c[d])}},prepend:function(a,c){if(1===a.nodeType){var d=a.firstChild;r(new V(c),function(c){a.insertBefore(c,d)})}},wrap:function(a,c){c=G(c).eq(0).clone()[0];var d=a.parentNode;d&&d.replaceChild(c,a);c.appendChild(a)},remove:xc,detach:function(a){xc(a,!0)},after:function(a,c){var d=a,e=a.parentNode;c=new V(c);for(var f=0,g=c.length;f<g;f++){var h=c[f];e.insertBefore(h,d.nextSibling);d=h}},addClass:Lb,removeClass:Kb,toggleClass:function(a,c,d){c&&r(c.split(" "),function(c){var f= +d;F(f)&&(f=!jb(a,c));(f?Lb:Kb)(a,c)})},parent:function(a){return(a=a.parentNode)&&11!==a.nodeType?a:null},next:function(a){return a.nextElementSibling},find:function(a,c){return a.getElementsByTagName?a.getElementsByTagName(c):[]},clone:Ib,triggerHandler:function(a,c,d){var e,f;e=c.type||c;var g=nb(a);if(g=(g=g&&g.events)&&g[e])e={preventDefault:function(){this.defaultPrevented=!0},isDefaultPrevented:function(){return!0===this.defaultPrevented},stopPropagation:w,type:e,target:a},c.type&&(e=E(e,c)), +c=qa(g),f=d?[e].concat(d):[e],r(c,function(c){c.apply(a,f)})}},function(a,c){V.prototype[c]=function(c,e,f){for(var g,h=0,m=this.length;h<m;h++)F(g)?(g=a(this[h],c,e,f),B(g)&&(g=G(g))):tc(g,a(this[h],c,e,f));return B(g)?g:this};V.prototype.bind=V.prototype.on;V.prototype.unbind=V.prototype.off});Xa.prototype={put:function(a,c){this[Ka(a,this.nextUid)]=c},get:function(a){return this[Ka(a,this.nextUid)]},remove:function(a){var c=this[a=Ka(a,this.nextUid)];delete this[a];return c}};var Cc=/^function\s*[^\(]*\(\s*([^\)]*)\)/m, +Xe=/,/,Ye=/^\s*(_?)(\S+?)\1\s*$/,Bc=/((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg,La=K("$injector");Cb.$$annotate=Mb;var uf=K("$animate"),oe=["$provide",function(a){this.$$selectors={};this.register=function(c,d){var e=c+"-animation";if(c&&"."!=c.charAt(0))throw uf("notcsel",c);this.$$selectors[c.substr(1)]=e;a.factory(e,d)};this.classNameFilter=function(a){1===arguments.length&&(this.$$classNameFilter=a instanceof RegExp?a:null);return this.$$classNameFilter};this.$get=["$$q","$$asyncCallback",function(a,d){function e(){f|| +(f=a.defer(),d(function(){f.resolve();f=null}));return f.promise}var f;return{enter:function(a,c,d){d?d.after(a):c.prepend(a);return e()},leave:function(a){a.remove();return e()},move:function(a,c,d){return this.enter(a,c,d)},addClass:function(a,c){c=C(c)?c:L(c)?c.join(" "):"";r(a,function(a){Lb(a,c)});return e()},removeClass:function(a,c){c=C(c)?c:L(c)?c.join(" "):"";r(a,function(a){Kb(a,c)});return e()},setClass:function(a,c,d){this.addClass(a,c);this.removeClass(a,d);return e()},enabled:w,cancel:w}}]}], +ja=K("$compile");jc.$inject=["$provide","$$sanitizeUriProvider"];var af=/^(x[\:\-_]|data[\:\-_])/i,Pb=K("$interpolate"),vf=/^([^\?#]*)(\?([^#]*))?(#(.*))?$/,df={http:80,https:443,ftp:21},sb=K("$location");Mc.prototype=Sb.prototype=Lc.prototype={$$html5:!1,$$replace:!1,absUrl:tb("$$absUrl"),url:function(a){if(F(a))return this.$$url;a=vf.exec(a);a[1]&&this.path(decodeURIComponent(a[1]));(a[2]||a[1])&&this.search(a[3]||"");this.hash(a[5]||"");return this},protocol:tb("$$protocol"),host:tb("$$host"), +port:tb("$$port"),path:Nc("$$path",function(a){a=a?a.toString():"";return"/"==a.charAt(0)?a:"/"+a}),search:function(a,c){switch(arguments.length){case 0:return this.$$search;case 1:if(C(a)||ea(a))a=a.toString(),this.$$search=fc(a);else if(S(a))r(a,function(c,e){null==c&&delete a[e]}),this.$$search=a;else throw sb("isrcharg");break;default:F(c)||null===c?delete this.$$search[a]:this.$$search[a]=c}this.$$compose();return this},hash:Nc("$$hash",function(a){return a?a.toString():""}),replace:function(){this.$$replace= +!0;return this}};var oa=K("$parse"),wf=Function.prototype.call,xf=Function.prototype.apply,yf=Function.prototype.bind,hd=Object.create(null);r({"null":function(){return null},"true":function(){return!0},"false":function(){return!1},undefined:function(){}},function(a,c){a.constant=a.literal=a.sharedGetter=!0;hd[c]=a});var Wb=E(Object.create(null),{"+":function(a,c,d,e){d=d(a,c);e=e(a,c);return B(d)?B(e)?d+e:d:B(e)?e:s},"-":function(a,c,d,e){d=d(a,c);e=e(a,c);return(B(d)?d:0)-(B(e)?e:0)},"*":function(a, +c,d,e){return d(a,c)*e(a,c)},"/":function(a,c,d,e){return d(a,c)/e(a,c)},"%":function(a,c,d,e){return d(a,c)%e(a,c)},"^":function(a,c,d,e){return d(a,c)^e(a,c)},"=":w,"===":function(a,c,d,e){return d(a,c)===e(a,c)},"!==":function(a,c,d,e){return d(a,c)!==e(a,c)},"==":function(a,c,d,e){return d(a,c)==e(a,c)},"!=":function(a,c,d,e){return d(a,c)!=e(a,c)},"<":function(a,c,d,e){return d(a,c)<e(a,c)},">":function(a,c,d,e){return d(a,c)>e(a,c)},"<=":function(a,c,d,e){return d(a,c)<=e(a,c)},">=":function(a, +c,d,e){return d(a,c)>=e(a,c)},"&&":function(a,c,d,e){return d(a,c)&&e(a,c)},"||":function(a,c,d,e){return d(a,c)||e(a,c)},"&":function(a,c,d,e){return d(a,c)&e(a,c)},"|":function(a,c,d,e){return e(a,c)(a,c,d(a,c))},"!":function(a,c,d){return!d(a,c)}}),zf={n:"\n",f:"\f",r:"\r",t:"\t",v:"\v","'":"'",'"':'"'},Tb=function(a){this.options=a};Tb.prototype={constructor:Tb,lex:function(a){this.text=a;this.index=0;this.ch=s;for(this.tokens=[];this.index<this.text.length;)if(this.ch=this.text.charAt(this.index), +this.is("\"'"))this.readString(this.ch);else if(this.isNumber(this.ch)||this.is(".")&&this.isNumber(this.peek()))this.readNumber();else if(this.isIdent(this.ch))this.readIdent();else if(this.is("(){}[].,;:?"))this.tokens.push({index:this.index,text:this.ch}),this.index++;else if(this.isWhitespace(this.ch))this.index++;else{a=this.ch+this.peek();var c=a+this.peek(2),d=Wb[this.ch],e=Wb[a],f=Wb[c];f?(this.tokens.push({index:this.index,text:c,fn:f}),this.index+=3):e?(this.tokens.push({index:this.index, +text:a,fn:e}),this.index+=2):d?(this.tokens.push({index:this.index,text:this.ch,fn:d}),this.index+=1):this.throwError("Unexpected next character ",this.index,this.index+1)}return this.tokens},is:function(a){return-1!==a.indexOf(this.ch)},peek:function(a){a=a||1;return this.index+a<this.text.length?this.text.charAt(this.index+a):!1},isNumber:function(a){return"0"<=a&&"9">=a},isWhitespace:function(a){return" "===a||"\r"===a||"\t"===a||"\n"===a||"\v"===a||"\u00a0"===a},isIdent:function(a){return"a"<= +a&&"z">=a||"A"<=a&&"Z">=a||"_"===a||"$"===a},isExpOperator:function(a){return"-"===a||"+"===a||this.isNumber(a)},throwError:function(a,c,d){d=d||this.index;c=B(c)?"s "+c+"-"+this.index+" ["+this.text.substring(c,d)+"]":" "+d;throw oa("lexerr",a,c,this.text);},readNumber:function(){for(var a="",c=this.index;this.index<this.text.length;){var d=P(this.text.charAt(this.index));if("."==d||this.isNumber(d))a+=d;else{var e=this.peek();if("e"==d&&this.isExpOperator(e))a+=d;else if(this.isExpOperator(d)&& +e&&this.isNumber(e)&&"e"==a.charAt(a.length-1))a+=d;else if(!this.isExpOperator(d)||e&&this.isNumber(e)||"e"!=a.charAt(a.length-1))break;else this.throwError("Invalid exponent")}this.index++}a*=1;this.tokens.push({index:c,text:a,constant:!0,fn:function(){return a}})},readIdent:function(){for(var a=this.text,c="",d=this.index,e,f,g,h;this.index<this.text.length;){h=this.text.charAt(this.index);if("."===h||this.isIdent(h)||this.isNumber(h))"."===h&&(e=this.index),c+=h;else break;this.index++}e&&"."=== +c[c.length-1]&&(this.index--,c=c.slice(0,-1),e=c.lastIndexOf("."),-1===e&&(e=s));if(e)for(f=this.index;f<this.text.length;){h=this.text.charAt(f);if("("===h){g=c.substr(e-d+1);c=c.substr(0,e-d);this.index=f;break}if(this.isWhitespace(h))f++;else break}this.tokens.push({index:d,text:c,fn:hd[c]||Pc(c,this.options,a)});g&&(this.tokens.push({index:e,text:"."}),this.tokens.push({index:e+1,text:g}))},readString:function(a){var c=this.index;this.index++;for(var d="",e=a,f=!1;this.index<this.text.length;){var g= +this.text.charAt(this.index),e=e+g;if(f)"u"===g?(f=this.text.substring(this.index+1,this.index+5),f.match(/[\da-f]{4}/i)||this.throwError("Invalid unicode escape [\\u"+f+"]"),this.index+=4,d+=String.fromCharCode(parseInt(f,16))):d+=zf[g]||g,f=!1;else if("\\"===g)f=!0;else{if(g===a){this.index++;this.tokens.push({index:c,text:e,string:d,constant:!0,fn:function(){return d}});return}d+=g}this.index++}this.throwError("Unterminated quote",c)}};var Za=function(a,c,d){this.lexer=a;this.$filter=c;this.options= +d};Za.ZERO=E(function(){return 0},{sharedGetter:!0,constant:!0});Za.prototype={constructor:Za,parse:function(a){this.text=a;this.tokens=this.lexer.lex(a);a=this.statements();0!==this.tokens.length&&this.throwError("is an unexpected token",this.tokens[0]);a.literal=!!a.literal;a.constant=!!a.constant;return a},primary:function(){var a;if(this.expect("("))a=this.filterChain(),this.consume(")");else if(this.expect("["))a=this.arrayDeclaration();else if(this.expect("{"))a=this.object();else{var c=this.expect(); +(a=c.fn)||this.throwError("not a primary expression",c);c.constant&&(a.constant=!0,a.literal=!0)}for(var d;c=this.expect("(","[",".");)"("===c.text?(a=this.functionCall(a,d),d=null):"["===c.text?(d=a,a=this.objectIndex(a)):"."===c.text?(d=a,a=this.fieldAccess(a)):this.throwError("IMPOSSIBLE");return a},throwError:function(a,c){throw oa("syntax",c.text,a,c.index+1,this.text,this.text.substring(c.index));},peekToken:function(){if(0===this.tokens.length)throw oa("ueoe",this.text);return this.tokens[0]}, +peek:function(a,c,d,e){if(0<this.tokens.length){var f=this.tokens[0],g=f.text;if(g===a||g===c||g===d||g===e||!(a||c||d||e))return f}return!1},expect:function(a,c,d,e){return(a=this.peek(a,c,d,e))?(this.tokens.shift(),a):!1},consume:function(a){this.expect(a)||this.throwError("is unexpected, expecting ["+a+"]",this.peek())},unaryFn:function(a,c){return E(function(d,e){return a(d,e,c)},{constant:c.constant})},ternaryFn:function(a,c,d){return E(function(e,f){return a(e,f)?c(e,f):d(e,f)},{constant:a.constant&& +c.constant&&d.constant})},binaryFn:function(a,c,d){return E(function(e,f){return c(e,f,a,d)},{constant:a.constant&&d.constant})},statements:function(){for(var a=[];;)if(0<this.tokens.length&&!this.peek("}",")",";","]")&&a.push(this.filterChain()),!this.expect(";"))return 1===a.length?a[0]:function(c,d){for(var e,f=0,g=a.length;f<g;f++)e=a[f](c,d);return e}},filterChain:function(){for(var a=this.expression(),c;c=this.expect("|");)a=this.binaryFn(a,c.fn,this.filter());return a},filter:function(){var a= +this.expect(),c=this.$filter(a.text),d,e;if(this.peek(":"))for(d=[],e=[];this.expect(":");)d.push(this.expression());return da(function(a,g,h){if(e){e[0]=h;for(h=d.length;h--;)e[h+1]=d[h](a,g);return c.apply(s,e)}return c(h)})},expression:function(){return this.assignment()},assignment:function(){var a=this.ternary(),c,d;return(d=this.expect("="))?(a.assign||this.throwError("implies assignment but ["+this.text.substring(0,d.index)+"] can not be assigned to",d),c=this.ternary(),function(d,f){return a.assign(d, +c(d,f),f)}):a},ternary:function(){var a=this.logicalOR(),c,d;if(this.expect("?")){c=this.assignment();if(d=this.expect(":"))return this.ternaryFn(a,c,this.assignment());this.throwError("expected :",d)}else return a},logicalOR:function(){for(var a=this.logicalAND(),c;c=this.expect("||");)a=this.binaryFn(a,c.fn,this.logicalAND());return a},logicalAND:function(){var a=this.equality(),c;if(c=this.expect("&&"))a=this.binaryFn(a,c.fn,this.logicalAND());return a},equality:function(){var a=this.relational(), +c;if(c=this.expect("==","!=","===","!=="))a=this.binaryFn(a,c.fn,this.equality());return a},relational:function(){var a=this.additive(),c;if(c=this.expect("<",">","<=",">="))a=this.binaryFn(a,c.fn,this.relational());return a},additive:function(){for(var a=this.multiplicative(),c;c=this.expect("+","-");)a=this.binaryFn(a,c.fn,this.multiplicative());return a},multiplicative:function(){for(var a=this.unary(),c;c=this.expect("*","/","%");)a=this.binaryFn(a,c.fn,this.unary());return a},unary:function(){var a; +return this.expect("+")?this.primary():(a=this.expect("-"))?this.binaryFn(Za.ZERO,a.fn,this.unary()):(a=this.expect("!"))?this.unaryFn(a.fn,this.unary()):this.primary()},fieldAccess:function(a){var c=this.text,d=this.expect().text,e=Pc(d,this.options,c);return E(function(c,d,h){return e(h||a(c,d))},{assign:function(e,g,h){(h=a(e,h))||a.assign(e,h={});return ub(h,d,g,c)}})},objectIndex:function(a){var c=this.text,d=this.expression();this.consume("]");return E(function(e,f){var g=a(e,f),h=d(e,f);na(h, +c);return g?Ba(g[h],c):s},{assign:function(e,f,g){var h=na(d(e,g),c);(g=Ba(a(e,g),c))||a.assign(e,g={});return g[h]=f}})},functionCall:function(a,c){var d=[];if(")"!==this.peekToken().text){do d.push(this.expression());while(this.expect(","))}this.consume(")");var e=this.text,f=d.length?[]:null;return function(g,h){var m=c?c(g,h):g,k=a(g,h,m)||w;if(f)for(var n=d.length;n--;)f[n]=Ba(d[n](g,h),e);Ba(m,e);if(k){if(k.constructor===k)throw oa("isecfn",e);if(k===wf||k===xf||k===yf)throw oa("isecff",e); +}m=k.apply?k.apply(m,f):k(f[0],f[1],f[2],f[3],f[4]);return Ba(m,e)}},arrayDeclaration:function(){var a=[],c=!0;if("]"!==this.peekToken().text){do{if(this.peek("]"))break;var d=this.expression();a.push(d);d.constant||(c=!1)}while(this.expect(","))}this.consume("]");return E(function(c,d){for(var g=[],h=0,m=a.length;h<m;h++)g.push(a[h](c,d));return g},{literal:!0,constant:c})},object:function(){var a=[],c=!0;if("}"!==this.peekToken().text){do{if(this.peek("}"))break;var d=this.expect(),d=d.string|| +d.text;this.consume(":");var e=this.expression();a.push({key:d,value:e});e.constant||(c=!1)}while(this.expect(","))}this.consume("}");return E(function(c,d){for(var e={},m=0,k=a.length;m<k;m++){var n=a[m];e[n.key]=n.value(c,d)}return e},{literal:!0,constant:c})}};var Qc=Object.create(null),Ca=K("$sce"),ka={HTML:"html",CSS:"css",URL:"url",RESOURCE_URL:"resourceUrl",JS:"js"},ja=K("$compile"),Z=Y.createElement("a"),Tc=Aa(t.location.href,!0);qc.$inject=["$provide"];Uc.$inject=["$locale"];Wc.$inject=["$locale"]; +var Zc=".",nf={yyyy:$("FullYear",4),yy:$("FullYear",2,0,!0),y:$("FullYear",1),MMMM:wb("Month"),MMM:wb("Month",!0),MM:$("Month",2,1),M:$("Month",1,1),dd:$("Date",2),d:$("Date",1),HH:$("Hours",2),H:$("Hours",1),hh:$("Hours",2,-12),h:$("Hours",1,-12),mm:$("Minutes",2),m:$("Minutes",1),ss:$("Seconds",2),s:$("Seconds",1),sss:$("Milliseconds",3),EEEE:wb("Day"),EEE:wb("Day",!0),a:function(a,c){return 12>a.getHours()?c.AMPMS[0]:c.AMPMS[1]},Z:function(a){a=-1*a.getTimezoneOffset();return a=(0<=a?"+":"")+(vb(Math[0< +a?"floor":"ceil"](a/60),2)+vb(Math.abs(a%60),2))},ww:ad(2),w:ad(1)},mf=/((?:[^yMdHhmsaZEw']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z|w+))(.*)/,lf=/^\-?\d+$/;Vc.$inject=["$locale"];var jf=da(P),kf=da(ib);Xc.$inject=["$parse"];var Fd=da({restrict:"E",compile:function(a,c){8>=X&&(c.href||c.name||c.$set("href",""),a.append(Y.createComment("IE fix")));if(!c.href&&!c.xlinkHref&&!c.name)return function(a,c){var f="[object SVGAnimatedString]"===Ga.call(c.prop("href"))?"xlink:href":"href";c.on("click", +function(a){c.attr(f)||a.preventDefault()})}}}),kb={};r(qb,function(a,c){if("multiple"!=a){var d=va("ng-"+c);kb[d]=function(){return{restrict:"A",priority:100,link:function(a,f,g){a.$watch(g[d],function(a){g.$set(c,!!a)})}}}}});r(Ac,function(a,c){kb[c]=function(){return{priority:100,link:function(a,e,f){if("ngPattern"===c&&"/"==f.ngPattern.charAt(0)&&(e=f.ngPattern.match(rf))){f.$set("ngPattern",new RegExp(e[1],e[2]));return}a.$watch(f[c],function(a){f.$set(c,a)})}}}});r(["src","srcset","href"],function(a){var c= +va("ng-"+a);kb[c]=function(){return{priority:99,link:function(d,e,f){var g=a,h=a;"href"===a&&"[object SVGAnimatedString]"===Ga.call(e.prop("href"))&&(h="xlinkHref",f.$attr[h]="xlink:href",g=null);f.$observe(c,function(c){c?(f.$set(h,c),X&&g&&e.prop(g,f[h])):"href"===a&&f.$set(h,null)})}}}});var xb={$addControl:w,$removeControl:w,$setValidity:w,$$setPending:w,$setDirty:w,$setPristine:w,$setSubmitted:w,$$clearControlValidity:w};bd.$inject=["$element","$attrs","$scope","$animate"];var id=function(a){return["$timeout", +function(c){return{name:"form",restrict:a?"EAC":"E",controller:bd,compile:function(){return{pre:function(a,e,f,g){if(!f.action){var h=function(c){a.$apply(function(){g.$commitViewValue();g.$setSubmitted()});c.preventDefault?c.preventDefault():c.returnValue=!1};e[0].addEventListener("submit",h,!1);e.on("$destroy",function(){c(function(){e[0].removeEventListener("submit",h,!1)},0,!1)})}var m=e.parent().controller("form"),k=f.name||f.ngForm;k&&ub(a,k,g,k);if(m)e.on("$destroy",function(){m.$removeControl(g); +k&&ub(a,k,s,k);E(g,xb)})}}}}}]},Gd=id(),Td=id(!0),of=/\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z)/,Af=/^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/,Bf=/^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i,Cf=/^\s*(\-|\+)?(\d+|(\d*(\.\d*)))\s*$/,jd=/^(\d{4})-(\d{2})-(\d{2})$/,kd=/^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d)(?::(\d\d))?$/,Xb=/^(\d{4})-W(\d\d)$/,ld=/^(\d{4})-(\d\d)$/,md=/^(\d\d):(\d\d)(?::(\d\d))?$/, +Df=/(\s+|^)default(\s+|$)/,Yb=new K("ngModel"),nd={text:function(a,c,d,e,f,g){$a(a,c,d,e,f,g);Ub(e)},date:ab("date",jd,zb(jd,["yyyy","MM","dd"]),"yyyy-MM-dd"),"datetime-local":ab("datetimelocal",kd,zb(kd,"yyyy MM dd HH mm ss".split(" ")),"yyyy-MM-ddTHH:mm:ss"),time:ab("time",md,zb(md,["HH","mm","ss"]),"HH:mm:ss"),week:ab("week",Xb,function(a){if(fa(a))return a;if(C(a)){Xb.lastIndex=0;var c=Xb.exec(a);if(c){a=+c[1];var d=+c[2],c=$c(a),d=7*(d-1);return new Date(a,0,c.getDate()+d)}}return NaN},"yyyy-Www"), +month:ab("month",ld,zb(ld,["yyyy","MM"]),"yyyy-MM"),number:function(a,c,d,e,f,g){dd(a,c,d,e);$a(a,c,d,e,f,g);e.$$parserName="number";e.$parsers.push(function(a){return e.$isEmpty(a)?null:Cf.test(a)?parseFloat(a):s});e.$formatters.push(function(a){if(!e.$isEmpty(a)){if(!ea(a))throw Yb("numfmt",a);a=a.toString()}return a});if(d.min||d.ngMin){var h;e.$validators.min=function(a){return e.$isEmpty(a)||F(h)||a>=h};d.$observe("min",function(a){B(a)&&!ea(a)&&(a=parseFloat(a,10));h=ea(a)&&!isNaN(a)?a:s;e.$validate()})}if(d.max|| +d.ngMax){var m;e.$validators.max=function(a){return e.$isEmpty(a)||F(m)||a<=m};d.$observe("max",function(a){B(a)&&!ea(a)&&(a=parseFloat(a,10));m=ea(a)&&!isNaN(a)?a:s;e.$validate()})}},url:function(a,c,d,e,f,g){$a(a,c,d,e,f,g);Ub(e);e.$$parserName="url";e.$validators.url=function(a,c){var d=a||c;return e.$isEmpty(d)||Af.test(d)}},email:function(a,c,d,e,f,g){$a(a,c,d,e,f,g);Ub(e);e.$$parserName="email";e.$validators.email=function(a,c){var d=a||c;return e.$isEmpty(d)||Bf.test(d)}},radio:function(a, +c,d,e){F(d.name)&&c.attr("name",++bb);c.on("click",function(a){c[0].checked&&e.$setViewValue(d.value,a&&a.type)});e.$render=function(){c[0].checked=d.value==e.$viewValue};d.$observe("value",e.$render)},checkbox:function(a,c,d,e,f,g,h,m){var k=ed(m,a,"ngTrueValue",d.ngTrueValue,!0),n=ed(m,a,"ngFalseValue",d.ngFalseValue,!1);c.on("click",function(a){e.$setViewValue(c[0].checked,a&&a.type)});e.$render=function(){c[0].checked=e.$viewValue};e.$isEmpty=function(a){return a!==k};e.$formatters.push(function(a){return ra(a, +k)});e.$parsers.push(function(a){return a?k:n})},hidden:w,button:w,submit:w,reset:w,file:w},kc=["$browser","$sniffer","$filter","$parse",function(a,c,d,e){return{restrict:"E",require:["?ngModel"],link:function(f,g,h,m){m[0]&&(nd[P(h.type)]||nd.text)(f,g,h,m[0],c,a,d,e)}}}],pf="ng-valid",qf="ng-invalid",Ma="ng-pristine",yb="ng-dirty",gd="ng-pending",Ef=["$scope","$exceptionHandler","$attrs","$element","$parse","$animate","$timeout","$rootScope","$q",function(a,c,d,e,f,g,h,m,k){this.$modelValue=this.$viewValue= +Number.NaN;this.$validators={};this.$asyncValidators={};this.$parsers=[];this.$formatters=[];this.$viewChangeListeners=[];this.$untouched=!0;this.$touched=!1;this.$pristine=!0;this.$dirty=!1;this.$valid=!0;this.$invalid=!1;this.$error={};this.$$success={};this.$pending=s;this.$name=d.name;var n=f(d.ngModel),p=null,l=this,q=function(){var c=n(a);l.$options&&l.$options.getterSetter&&D(c)&&(c=c());return c},A=function(c){var d;l.$options&&l.$options.getterSetter&&D(d=n(a))?d(l.$modelValue):n.assign(a, +l.$modelValue)};this.$$setOptions=function(a){l.$options=a;if(!(n.assign||a&&a.getterSetter))throw Yb("nonassign",d.ngModel,ta(e));};this.$render=w;this.$isEmpty=function(a){return F(a)||""===a||null===a||a!==a};var u=e.inheritedData("$formController")||xb,x=0;e.addClass(Ma).addClass("ng-untouched");cd({ctrl:this,$element:e,set:function(a,c){a[c]=!0},unset:function(a,c){delete a[c]},parentForm:u,$animate:g});this.$setPristine=function(){l.$dirty=!1;l.$pristine=!0;g.removeClass(e,yb);g.addClass(e, +Ma)};this.$setUntouched=function(){l.$touched=!1;l.$untouched=!0;g.setClass(e,"ng-untouched","ng-touched")};this.$setTouched=function(){l.$touched=!0;l.$untouched=!1;g.setClass(e,"ng-touched","ng-untouched")};this.$rollbackViewValue=function(){h.cancel(p);l.$viewValue=l.$$lastCommittedViewValue;l.$render()};this.$validate=function(){ea(l.$modelValue)&&isNaN(l.$modelValue)||this.$$parseAndValidate()};this.$$runValidators=function(a,c,d,e){function f(){var a=!0;r(l.$validators,function(e,f){var h=e(c, +d);a=a&&h;g(f,h)});return a?!0:(r(l.$asyncValidators,function(a,c){g(c,null)}),m(),!1)}function h(){var a=[];r(l.$asyncValidators,function(e,f){var h=e(c,d);if(!h||!D(h.then))throw Yb("$asyncValidators",h);g(f,s);a.push(h.then(function(){g(f,!0)},function(a){g(f,!1)}))});a.length?k.all(a).then(m):m()}function g(a,c){n===x&&l.$setValidity(a,c)}function m(){n===x&&e()}x++;var n=x;(function(a){var c=l.$$parserName||"parse";if(a===s)g(c,null);else if(g(c,a),!a)return r(l.$validators,function(a,c){g(c, +null)}),r(l.$asyncValidators,function(a,c){g(c,null)}),m(),!1;return!0})(a)&&f()&&h()};this.$commitViewValue=function(){var a=l.$viewValue;h.cancel(p);if(l.$$lastCommittedViewValue!==a||""===a&&l.$$hasNativeValidators)l.$$lastCommittedViewValue=a,l.$pristine&&(l.$dirty=!0,l.$pristine=!1,g.removeClass(e,Ma),g.addClass(e,yb),u.$setDirty()),this.$$parseAndValidate()};this.$$parseAndValidate=function(){for(var a=!0,c=l.$$lastCommittedViewValue,d=c,e=0;e<l.$parsers.length;e++)if(d=l.$parsers[e](d),F(d)){a= +!1;break}ea(l.$modelValue)&&isNaN(l.$modelValue)&&(l.$modelValue=q());var f=l.$modelValue,h=l.$options&&l.$options.allowInvalid;h&&(l.$modelValue=d,l.$modelValue!==f&&l.$$writeModelToScope());l.$$runValidators(a,d,c,function(){h||(l.$modelValue=l.$valid?d:s,l.$modelValue!==f&&l.$$writeModelToScope())})};this.$$writeModelToScope=function(){A(l.$modelValue);r(l.$viewChangeListeners,function(a){try{a()}catch(d){c(d)}})};this.$setViewValue=function(a,c){l.$viewValue=a;l.$options&&!l.$options.updateOnDefault|| +l.$$debounceViewValueCommit(c)};this.$$debounceViewValueCommit=function(c){var d=0,e=l.$options;e&&B(e.debounce)&&(e=e.debounce,ea(e)?d=e:ea(e[c])?d=e[c]:ea(e["default"])&&(d=e["default"]));h.cancel(p);d?p=h(function(){l.$commitViewValue()},d):m.$$phase?l.$commitViewValue():a.$apply(function(){l.$commitViewValue()})};a.$watch(function(){var a=q();if(a!==l.$modelValue){l.$modelValue=a;for(var c=l.$formatters,d=c.length,e=a;d--;)e=c[d](e);l.$viewValue!==e&&(l.$viewValue=l.$$lastCommittedViewValue=e, +l.$render(),l.$$runValidators(s,a,e,w))}return a})}],he=function(){return{restrict:"A",require:["ngModel","^?form","^?ngModelOptions"],controller:Ef,link:{pre:function(a,c,d,e){var f=e[0],g=e[1]||xb;f.$$setOptions(e[2]&&e[2].$options);g.$addControl(f);a.$on("$destroy",function(){g.$removeControl(f)})},post:function(a,c,d,e){var f=e[0];if(f.$options&&f.$options.updateOn)c.on(f.$options.updateOn,function(a){f.$$debounceViewValueCommit(a&&a.type)});c.on("blur",function(c){f.$touched||a.$apply(function(){f.$setTouched()})})}}}}, +je=da({restrict:"A",require:"ngModel",link:function(a,c,d,e){e.$viewChangeListeners.push(function(){a.$eval(d.ngChange)})}}),mc=function(){return{restrict:"A",require:"?ngModel",link:function(a,c,d,e){e&&(d.required=!0,e.$validators.required=function(a,c){return!d.required||!e.$isEmpty(c)},d.$observe("required",function(){e.$validate()}))}}},lc=function(){return{restrict:"A",require:"?ngModel",link:function(a,c,d,e){if(e){var f,g=d.ngPattern||d.pattern;d.$observe("pattern",function(a){C(a)&&0<a.length&& +(a=new RegExp(a));if(a&&!a.test)throw K("ngPattern")("noregexp",g,a,ta(c));f=a||s;e.$validate()});e.$validators.pattern=function(a){return e.$isEmpty(a)||F(f)||f.test(a)}}}}},oc=function(){return{restrict:"A",require:"?ngModel",link:function(a,c,d,e){if(e){var f=0;d.$observe("maxlength",function(a){f=U(a)||0;e.$validate()});e.$validators.maxlength=function(a,c){return e.$isEmpty(c)||c.length<=f}}}}},nc=function(){return{restrict:"A",require:"?ngModel",link:function(a,c,d,e){if(e){var f=0;d.$observe("minlength", +function(a){f=U(a)||0;e.$validate()});e.$validators.minlength=function(a,c){return e.$isEmpty(c)||c.length>=f}}}}},ie=function(){return{restrict:"A",priority:100,require:"ngModel",link:function(a,c,d,e){var f=c.attr(d.$attr.ngList)||", ",g="false"!==d.ngTrim,h=g?ba(f):f;e.$parsers.push(function(a){if(!F(a)){var c=[];a&&r(a.split(h),function(a){a&&c.push(g?ba(a):a)});return c}});e.$formatters.push(function(a){return L(a)?a.join(f):s});e.$isEmpty=function(a){return!a||!a.length}}}},Ff=/^(true|false|\d+)$/, +ke=function(){return{restrict:"A",priority:100,compile:function(a,c){return Ff.test(c.ngValue)?function(a,c,f){f.$set("value",a.$eval(f.ngValue))}:function(a,c,f){a.$watch(f.ngValue,function(a){f.$set("value",a)})}}}},le=function(){return{restrict:"A",controller:["$scope","$attrs",function(a,c){var d=this;this.$options=a.$eval(c.ngModelOptions);this.$options.updateOn!==s?(this.$options.updateOnDefault=!1,this.$options.updateOn=ba(this.$options.updateOn.replace(Df,function(){d.$options.updateOnDefault= +!0;return" "}))):this.$options.updateOnDefault=!0}]}},Ld=["$compile",function(a){return{restrict:"AC",compile:function(c){a.$$addBindingClass(c);return function(c,e,f){a.$$addBindingInfo(e,f.ngBind);c.$watch(f.ngBind,function(a){e.text(a==s?"":a)})}}}}],Nd=["$interpolate","$compile",function(a,c){return{compile:function(d){c.$$addBindingClass(d);return function(d,f,g){d=a(f.attr(g.$attr.ngBindTemplate));c.$$addBindingInfo(f,d.expressions);g.$observe("ngBindTemplate",function(a){f.text(a)})}}}}],Md= +["$sce","$parse","$compile",function(a,c,d){return{restrict:"A",compile:function(e,f){var g=c(f.ngBindHtml),h=c(f.ngBindHtml,function(a){return(a||"").toString()});d.$$addBindingClass(e);return function(c,e,f){d.$$addBindingInfo(e,f.ngBindHtml);c.$watch(h,function(){e.html(a.getTrustedHtml(g(c))||"")})}}}}],Od=Vb("",!0),Qd=Vb("Odd",0),Pd=Vb("Even",1),Rd=Fa({compile:function(a,c){c.$set("ngCloak",s);a.removeClass("ng-cloak")}}),Sd=[function(){return{restrict:"A",scope:!0,controller:"@",priority:500}}], +pc={},Gf={blur:!0,focus:!0};r("click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste".split(" "),function(a){var c=va("ng-"+a);pc[c]=["$parse","$rootScope",function(d,e){return{restrict:"A",compile:function(f,g){var h=d(g[c]);return function(c,d){var f=P(a);d.on(f,function(a){var d=function(){h(c,{$event:a})};Gf[f]&&e.$$phase?c.$evalAsync(d):c.$apply(d)})}}}}]});var Vd=["$animate",function(a){return{multiElement:!0, +transclude:"element",priority:600,terminal:!0,restrict:"A",$$tlb:!0,link:function(c,d,e,f,g){var h,m,k;c.$watch(e.ngIf,function(c){c?m||g(function(c,f){m=f;c[c.length++]=Y.createComment(" end ngIf: "+e.ngIf+" ");h={clone:c};a.enter(c,d.parent(),d)}):(k&&(k.remove(),k=null),m&&(m.$destroy(),m=null),h&&(k=hb(h.clone),a.leave(k).then(function(){k=null}),h=null))})}}}],Wd=["$templateRequest","$anchorScroll","$animate","$sce",function(a,c,d,e){return{restrict:"ECA",priority:400,terminal:!0,transclude:"element", +controller:Ea.noop,compile:function(f,g){var h=g.ngInclude||g.src,m=g.onload||"",k=g.autoscroll;return function(f,g,l,q,r){var u=0,s,z,t,v=function(){z&&(z.remove(),z=null);s&&(s.$destroy(),s=null);t&&(d.leave(t).then(function(){z=null}),z=t,t=null)};f.$watch(e.parseAsResourceUrl(h),function(e){var h=function(){!B(k)||k&&!f.$eval(k)||c()},l=++u;e?(a(e,!0).then(function(a){if(l===u){var c=f.$new();q.template=a;a=r(c,function(a){v();d.enter(a,null,g).then(h)});s=c;t=a;s.$emit("$includeContentLoaded"); +f.$eval(m)}},function(){l===u&&(v(),f.$emit("$includeContentError"))}),f.$emit("$includeContentRequested")):(v(),q.template=null)})}}}}],me=["$compile",function(a){return{restrict:"ECA",priority:-400,require:"ngInclude",link:function(c,d,e,f){/SVG/.test(d[0].toString())?(d.empty(),a(sc(f.template,Y).childNodes)(c,function(a){d.append(a)},s,s,d)):(d.html(f.template),a(d.contents())(c))}}}],Xd=Fa({priority:450,compile:function(){return{pre:function(a,c,d){a.$eval(d.ngInit)}}}}),Yd=Fa({terminal:!0,priority:1E3}), +Zd=["$locale","$interpolate",function(a,c){var d=/{}/g;return{restrict:"EA",link:function(e,f,g){var h=g.count,m=g.$attr.when&&f.attr(g.$attr.when),k=g.offset||0,n=e.$eval(m)||{},p={},l=c.startSymbol(),q=c.endSymbol(),s=/^when(Minus)?(.+)$/;r(g,function(a,c){s.test(c)&&(n[P(c.replace("when","").replace("Minus","-"))]=f.attr(g.$attr[c]))});r(n,function(a,e){p[e]=c(a.replace(d,l+h+"-"+k+q))});e.$watch(function(){var c=parseFloat(e.$eval(h));if(isNaN(c))return"";c in n||(c=a.pluralCat(c-k));return p[c](e)}, +function(a){f.text(a)})}}}],$d=["$parse","$animate",function(a,c){var d=K("ngRepeat"),e=function(a,c,d,e,k,n,p){a[d]=e;k&&(a[k]=n);a.$index=c;a.$first=0===c;a.$last=c===p-1;a.$middle=!(a.$first||a.$last);a.$odd=!(a.$even=0===(c&1))};return{restrict:"A",multiElement:!0,transclude:"element",priority:1E3,terminal:!0,$$tlb:!0,compile:function(f,g){var h=g.ngRepeat,m=Y.createComment(" end ngRepeat: "+h+" "),k=h.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+track\s+by\s+([\s\S]+?))?\s*$/); +if(!k)throw d("iexp",h);var n=k[1],p=k[2],l=k[3],q=k[4],k=n.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/);if(!k)throw d("iidexp",n);var A=k[3]||k[1],u=k[2];if(l&&(!/^[$a-zA-Z_][$a-zA-Z0-9_]*$/.test(l)||/^(null|undefined|this|\$index|\$first|\$middle|\$last|\$even|\$odd|\$parent)$/.test(l)))throw d("badident",l);var t,z,B,v,w={$id:Ka};q?t=a(q):(B=function(a,c){return Ka(c)},v=function(a){return a});return function(a,f,g,k,n){t&&(z=function(c,d,e){u&&(w[u]=c);w[A]=d;w.$index=e;return t(a,w)}); +var q=Object.create(null);a.$watchCollection(p,function(g){var k,p,t=f[0],J,x=Object.create(null),w,N,E,F,y,C,ga;l&&(a[l]=g);if(Na(g))y=g,p=z||B;else{p=z||v;y=[];for(ga in g)g.hasOwnProperty(ga)&&"$"!=ga.charAt(0)&&y.push(ga);y.sort()}w=y.length;ga=Array(w);for(k=0;k<w;k++)if(N=g===y?k:y[k],E=g[N],F=p(N,E,k),q[F])C=q[F],delete q[F],x[F]=C,ga[k]=C;else{if(x[F])throw r(ga,function(a){a&&a.scope&&(q[a.id]=a)}),d("dupes",h,F,sa(E));ga[k]={id:F,scope:s,clone:s};x[F]=!0}for(J in q){C=q[J];F=hb(C.clone); +c.leave(F);if(F[0].parentNode)for(k=0,p=F.length;k<p;k++)F[k].$$NG_REMOVED=!0;C.scope.$destroy()}for(k=0;k<w;k++)if(N=g===y?k:y[k],E=g[N],C=ga[k],C.scope){J=t;do J=J.nextSibling;while(J&&J.$$NG_REMOVED);C.clone[0]!=J&&c.move(hb(C.clone),null,G(t));t=C.clone[C.clone.length-1];e(C.scope,k,A,E,u,N,w)}else n(function(a,d){C.scope=d;var f=m.cloneNode(!1);a[a.length++]=f;c.enter(a,null,G(t));t=f;C.clone=a;x[C.id]=C;e(C.scope,k,A,E,u,N,w)});q=x})}}}}],ae=["$animate",function(a){return{restrict:"A",multiElement:!0, +link:function(c,d,e){c.$watch(e.ngShow,function(c){a[c?"removeClass":"addClass"](d,"ng-hide")})}}}],Ud=["$animate",function(a){return{restrict:"A",multiElement:!0,link:function(c,d,e){c.$watch(e.ngHide,function(c){a[c?"addClass":"removeClass"](d,"ng-hide")})}}}],be=Fa(function(a,c,d){a.$watch(d.ngStyle,function(a,d){d&&a!==d&&r(d,function(a,d){c.css(d,"")});a&&c.css(a)},!0)}),ce=["$animate",function(a){return{restrict:"EA",require:"ngSwitch",controller:["$scope",function(){this.cases={}}],link:function(c, +d,e,f){var g=[],h=[],m=[],k=[],n=function(a,c){return function(){a.splice(c,1)}};c.$watch(e.ngSwitch||e.on,function(c){var d,e;d=0;for(e=m.length;d<e;++d)a.cancel(m[d]);d=m.length=0;for(e=k.length;d<e;++d){var s=hb(h[d].clone);k[d].$destroy();(m[d]=a.leave(s)).then(n(m,d))}h.length=0;k.length=0;(g=f.cases["!"+c]||f.cases["?"])&&r(g,function(c){c.transclude(function(d,e){k.push(e);var f=c.element;d[d.length++]=Y.createComment(" end ngSwitchWhen: ");h.push({clone:d});a.enter(d,f.parent(),f)})})})}}}], +de=Fa({transclude:"element",priority:1200,require:"^ngSwitch",multiElement:!0,link:function(a,c,d,e,f){e.cases["!"+d.ngSwitchWhen]=e.cases["!"+d.ngSwitchWhen]||[];e.cases["!"+d.ngSwitchWhen].push({transclude:f,element:c})}}),ee=Fa({transclude:"element",priority:1200,require:"^ngSwitch",multiElement:!0,link:function(a,c,d,e,f){e.cases["?"]=e.cases["?"]||[];e.cases["?"].push({transclude:f,element:c})}}),ge=Fa({restrict:"EAC",link:function(a,c,d,e,f){if(!f)throw K("ngTransclude")("orphan",ta(c));f(function(a){c.empty(); +c.append(a)})}}),Hd=["$templateCache",function(a){return{restrict:"E",terminal:!0,compile:function(c,d){"text/ng-template"==d.type&&a.put(d.id,c[0].text)}}}],Hf=K("ngOptions"),fe=da({restrict:"A",terminal:!0}),Id=["$compile","$parse",function(a,c){var d=/^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\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]+?))?$/,e={$setViewValue:w};return{restrict:"E",require:["select", +"?ngModel"],controller:["$element","$scope","$attrs",function(a,c,d){var m=this,k={},n=e,p;m.databound=d.ngModel;m.init=function(a,c,d){n=a;p=d};m.addOption=function(c,d){Ja(c,'"option value"');k[c]=!0;n.$viewValue==c&&(a.val(c),p.parent()&&p.remove());d[0].hasAttribute("selected")&&(d[0].selected=!0)};m.removeOption=function(a){this.hasOption(a)&&(delete k[a],n.$viewValue==a&&this.renderUnknownOption(a))};m.renderUnknownOption=function(c){c="? "+Ka(c)+" ?";p.val(c);a.prepend(p);a.val(c);p.prop("selected", +!0)};m.hasOption=function(a){return k.hasOwnProperty(a)};c.$on("$destroy",function(){m.renderUnknownOption=w})}],link:function(e,g,h,m){function k(a,c,d,e){d.$render=function(){var a=d.$viewValue;e.hasOption(a)?(y.parent()&&y.remove(),c.val(a),""===a&&w.prop("selected",!0)):F(a)&&w?c.val(""):e.renderUnknownOption(a)};c.on("change",function(){a.$apply(function(){y.parent()&&y.remove();d.$setViewValue(c.val())})})}function n(a,c,d){var e;d.$render=function(){var a=new Xa(d.$viewValue);r(c.find("option"), +function(c){c.selected=B(a.get(c.value))})};a.$watch(function(){ra(e,d.$viewValue)||(e=qa(d.$viewValue),d.$render())});c.on("change",function(){a.$apply(function(){var a=[];r(c.find("option"),function(c){c.selected&&a.push(c.value)});d.$setViewValue(a)})})}function p(e,f,h){function g(){z||(e.$$postDigest(k),z=!0)}function k(){z=!1;var a={"":[]},c=[""],d,g,l,s,t;l=h.$modelValue;s=F(e)||[];var A=p?Zb(s):s,G,y,D;y={};D=!1;if(q)if(g=h.$modelValue,x&&L(g))for(D=new Xa([]),d={},t=0;t<g.length;t++)d[n]= +g[t],D.put(x(e,d),g[t]);else D=new Xa(g);t=D;var H,K;for(D=0;G=A.length,D<G;D++){g=D;if(p){g=A[D];if("$"===g.charAt(0))continue;y[p]=g}y[n]=s[g];d=r(e,y)||"";(g=a[d])||(g=a[d]=[],c.push(d));q?d=B(t.remove(x?x(e,y):w(e,y))):(x?(d={},d[n]=l,d=x(e,d)===x(e,y)):d=l===w(e,y),t=t||d);H=m(e,y);H=B(H)?H:"";g.push({id:x?x(e,y):p?A[D]:D,label:H,selected:d})}q||(u||null===l?a[""].unshift({id:"",label:"",selected:!t}):t||a[""].unshift({id:"?",label:"",selected:!0}));y=0;for(A=c.length;y<A;y++){d=c[y];g=a[d]; +E.length<=y?(l={element:v.clone().attr("label",d),label:g.label},s=[l],E.push(s),f.append(l.element)):(s=E[y],l=s[0],l.label!=d&&l.element.attr("label",l.label=d));H=null;D=0;for(G=g.length;D<G;D++)d=g[D],(t=s[D+1])?(H=t.element,t.label!==d.label&&H.text(t.label=d.label),t.id!==d.id&&H.val(t.id=d.id),H[0].selected!==d.selected&&(H.prop("selected",t.selected=d.selected),X&&H.prop("selected",t.selected))):(""===d.id&&u?K=u:(K=C.clone()).val(d.id).prop("selected",d.selected).attr("selected",d.selected).text(d.label), +s.push({element:K,label:d.label,id:d.id,selected:d.selected}),H?H.after(K):l.element.append(K),H=K);for(D++;s.length>D;)s.pop().element.remove()}for(;E.length>y;)E.pop()[0].element.remove()}var l;if(!(l=t.match(d)))throw Hf("iexp",t,ta(f));var m=c(l[2]||l[1]),n=l[4]||l[6],p=l[5],r=c(l[3]||""),w=c(l[2]?l[1]:n),F=c(l[7]),x=l[8]?c(l[8]):null,E=[[{element:f,label:""}]];u&&(a(u)(e),u.removeClass("ng-scope"),u.remove());f.empty();f.on("change",function(){e.$apply(function(){var a,c=F(e)||[],d={},g,l,m, +r,t,u,v;if(q)for(l=[],r=0,u=E.length;r<u;r++)for(a=E[r],m=1,t=a.length;m<t;m++){if((g=a[m].element)[0].selected){g=g.val();p&&(d[p]=g);if(x)for(v=0;v<c.length&&(d[n]=c[v],x(e,d)!=g);v++);else d[n]=c[g];l.push(w(e,d))}}else if(g=f.val(),"?"==g)l=s;else if(""===g)l=null;else if(x)for(v=0;v<c.length;v++){if(d[n]=c[v],x(e,d)==g){l=w(e,d);break}}else d[n]=c[g],p&&(d[p]=g),l=w(e,d);h.$setViewValue(l);k()})});h.$render=k;e.$watchCollection(F,g);q&&e.$watchCollection(function(){return h.$modelValue},g)}if(m[1]){var l= +m[0];m=m[1];var q=h.multiple,t=h.ngOptions,u=!1,w,z=!1,C=G(Y.createElement("option")),v=G(Y.createElement("optgroup")),y=C.clone();h=0;for(var E=g.children(),D=E.length;h<D;h++)if(""===E[h].value){w=u=E.eq(h);break}l.init(m,u,y);q&&(m.$isEmpty=function(a){return!a||0===a.length});t?p(e,g,m):q?n(e,g,m):k(e,g,m,l)}}}}],Kd=["$interpolate",function(a){var c={addOption:w,removeOption:w};return{restrict:"E",priority:100,compile:function(d,e){if(F(e.value)){var f=a(d.text(),!0);f||e.$set("value",d.text())}return function(a, +d,e){var k=d.parent(),n=k.data("$selectController")||k.parent().data("$selectController");n&&n.databound?d.prop("selected",!1):n=c;f?a.$watch(f,function(a,c){e.$set("value",a);c!==a&&n.removeOption(c);n.addOption(a,d)}):n.addOption(e.value,d);d.on("$destroy",function(){n.removeOption(e.value)})}}}}],Jd=da({restrict:"E",terminal:!1});t.angular.bootstrap?console.log("WARNING: Tried to load angular more than once."):(zd(),Bd(Ea),G(Y).ready(function(){vd(Y,gc)}))})(window,document); +!window.angular.$$csp()&&window.angular.element(document).find("head").prepend('<style type="text/css">@charset "UTF-8";[ng\\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak,.ng-hide:not(.ng-animate){display:none !important;}ng\\:form{display:block;}</style>'); //# sourceMappingURL=angular.min.js.map diff --git a/webclient/js/autofill-event.js b/webclient/js/autofill-event.js new file mode 100755 index 0000000000..006f83e1be --- /dev/null +++ b/webclient/js/autofill-event.js @@ -0,0 +1,117 @@ +/** + * Autofill event polyfill ##version:1.0.0## + * (c) 2014 Google, Inc. + * License: MIT + */ +(function(window) { + var $ = window.jQuery || window.angular.element; + var rootElement = window.document.documentElement, + $rootElement = $(rootElement); + + addGlobalEventListener('change', markValue); + addValueChangeByJsListener(markValue); + + $.prototype.checkAndTriggerAutoFillEvent = jqCheckAndTriggerAutoFillEvent; + + // Need to use blur and not change event + // as Chrome does not fire change events in all cases an input is changed + // (e.g. when starting to type and then finish the input by auto filling a username) + addGlobalEventListener('blur', function(target) { + // setTimeout needed for Chrome as it fills other + // form fields a little later... + window.setTimeout(function() { + findParentForm(target).find('input').checkAndTriggerAutoFillEvent(); + }, 20); + }); + + window.document.addEventListener('DOMContentLoaded', function() { + // The timeout is needed for Chrome as it auto fills + // login forms some time after DOMContentLoaded! + window.setTimeout(function() { + $rootElement.find('input').checkAndTriggerAutoFillEvent(); + }, 200); + }, false); + + return; + + // ---------- + + function jqCheckAndTriggerAutoFillEvent() { + var i, el; + for (i=0; i<this.length; i++) { + el = this[i]; + if (!valueMarked(el)) { + markValue(el); + triggerChangeEvent(el); + } + } + } + + function valueMarked(el) { + var val = el.value, + $$currentValue = el.$$currentValue; + if (!val && !$$currentValue) { + return true; + } + return val === $$currentValue; + } + + function markValue(el) { + el.$$currentValue = el.value; + } + + function addValueChangeByJsListener(listener) { + var jq = window.jQuery || window.angular.element, + jqProto = jq.prototype; + var _val = jqProto.val; + jqProto.val = function(newValue) { + var res = _val.apply(this, arguments); + if (arguments.length > 0) { + forEach(this, function(el) { + listener(el, newValue); + }); + } + return res; + } + } + + function addGlobalEventListener(eventName, listener) { + // Use a capturing event listener so that + // we also get the event when it's stopped! + // Also, the blur event does not bubble. + rootElement.addEventListener(eventName, onEvent, true); + + function onEvent(event) { + var target = event.target; + listener(target); + } + } + + function findParentForm(el) { + while (el) { + if (el.nodeName === 'FORM') { + return $(el); + } + el = el.parentNode; + } + return $(); + } + + function forEach(arr, listener) { + if (arr.forEach) { + return arr.forEach(listener); + } + var i; + for (i=0; i<arr.length; i++) { + listener(arr[i]); + } + } + + function triggerChangeEvent(element) { + var doc = window.document; + var event = doc.createEvent("HTMLEvents"); + event.initEvent("change", true, true); + element.dispatchEvent(event); + } + +})(window); diff --git a/webclient/media/busy.mp3 b/webclient/media/busy.mp3 new file mode 100644 index 0000000000..fec27ba4c5 --- /dev/null +++ b/webclient/media/busy.mp3 Binary files differdiff --git a/webclient/media/busy.ogg b/webclient/media/busy.ogg new file mode 100644 index 0000000000..5d64a7d0d9 --- /dev/null +++ b/webclient/media/busy.ogg Binary files differdiff --git a/webclient/media/callend.mp3 b/webclient/media/callend.mp3 new file mode 100644 index 0000000000..50c34e5640 --- /dev/null +++ b/webclient/media/callend.mp3 Binary files differdiff --git a/webclient/media/callend.ogg b/webclient/media/callend.ogg new file mode 100644 index 0000000000..927ce1f634 --- /dev/null +++ b/webclient/media/callend.ogg Binary files differdiff --git a/webclient/media/ring.mp3 b/webclient/media/ring.mp3 new file mode 100644 index 0000000000..3c3cdde3f9 --- /dev/null +++ b/webclient/media/ring.mp3 Binary files differdiff --git a/webclient/media/ring.ogg b/webclient/media/ring.ogg new file mode 100644 index 0000000000..de49b8ae6f --- /dev/null +++ b/webclient/media/ring.ogg Binary files differdiff --git a/webclient/media/ringback.mp3 b/webclient/media/ringback.mp3 new file mode 100644 index 0000000000..6ee34bf395 --- /dev/null +++ b/webclient/media/ringback.mp3 Binary files differdiff --git a/webclient/media/ringback.ogg b/webclient/media/ringback.ogg new file mode 100644 index 0000000000..7dbfdcd017 --- /dev/null +++ b/webclient/media/ringback.ogg Binary files differdiff --git a/webclient/recents/recents-controller.js b/webclient/recents/recents-controller.js index 0f27f7a660..a0db0538f3 100644 --- a/webclient/recents/recents-controller.js +++ b/webclient/recents/recents-controller.js @@ -17,80 +17,133 @@ 'use strict'; angular.module('RecentsController', ['matrixService', 'matrixFilter', 'eventHandlerService']) -.controller('RecentsController', ['$scope', 'matrixService', 'eventHandlerService', - function($scope, matrixService, eventHandlerService) { - $scope.rooms = {}; +.controller('RecentsController', ['$rootScope', '$scope', 'matrixService', 'eventHandlerService', + function($rootScope, $scope, matrixService, eventHandlerService) { + + // FIXME: Angularjs reloads the controller (and resets its $scope) each time + // the page URL changes, use $rootScope to avoid to have to reload data + $rootScope.rooms; - // $scope of the parent where the recents component is included can override this value + // $rootScope of the parent where the recents component is included can override this value // in order to highlight a specific room in the list - $scope.recentsSelectedRoomID; - + $rootScope.recentsSelectedRoomID; + var listenToEventStream = function() { // Refresh the list on matrix invitation and message event - $scope.$on(eventHandlerService.MEMBER_EVENT, function(ngEvent, event, isLive) { + $rootScope.$on(eventHandlerService.MEMBER_EVENT, function(ngEvent, event, isLive) { + if (isLive) { + if (!$rootScope.rooms[event.room_id]) { + // The user has joined a new room, which we do not have data yet. The reason is that + // the room has appeared in the scope of the user rooms after the global initialSync + // FIXME: an initialSync on this specific room should be done + $rootScope.rooms[event.room_id] = { + room_id:event.room_id + }; + } + else if (event.state_key === matrixService.config().user_id && "invite" !== event.membership && "join" !== event.membership) { + // The user has been kicked or banned from the room, remove this room from the recents + delete $rootScope.rooms[event.room_id]; + } + + if ($rootScope.rooms[event.room_id]) { + $rootScope.rooms[event.room_id].lastMsg = event; + } + + // Update room users count + $rootScope.rooms[event.room_id].numUsersInRoom = getUsersCountInRoom(event.room_id); + } + }); + $rootScope.$on(eventHandlerService.MSG_EVENT, function(ngEvent, event, isLive) { + if (isLive) { + $rootScope.rooms[event.room_id].lastMsg = event; + } + }); + $rootScope.$on(eventHandlerService.CALL_EVENT, function(ngEvent, event, isLive) { if (isLive) { - $scope.rooms[event.room_id].lastMsg = event; + $rootScope.rooms[event.room_id].lastMsg = event; } }); - $scope.$on(eventHandlerService.MSG_EVENT, function(ngEvent, event, isLive) { + $rootScope.$on(eventHandlerService.ROOM_CREATE_EVENT, function(ngEvent, event, isLive) { if (isLive) { - $scope.rooms[event.room_id].lastMsg = event; + $rootScope.rooms[event.room_id] = event; } }); - $scope.$on(eventHandlerService.CALL_EVENT, function(ngEvent, event, isLive) { + $rootScope.$on(eventHandlerService.NAME_EVENT, function(ngEvent, event, isLive) { if (isLive) { - $scope.rooms[event.room_id].lastMsg = event; + $rootScope.rooms[event.room_id].lastMsg = event; } }); - $scope.$on(eventHandlerService.ROOM_CREATE_EVENT, function(ngEvent, event, isLive) { + $rootScope.$on(eventHandlerService.TOPIC_EVENT, function(ngEvent, event, isLive) { if (isLive) { - $scope.rooms[event.room_id] = event; + $rootScope.rooms[event.room_id].lastMsg = event; } }); }; - - var refresh = function() { - // List all rooms joined or been invited to - // TODO: This is a pity that event-stream-service.js makes the same call - // We should be able to reuse event-stream-service.js fetched data - matrixService.rooms(1, false).then( - function(response) { - // Reset data - $scope.rooms = {}; + /** + * Compute the room users number, ie the number of members who has joined the room. + * @param {String} room_id the room id + * @returns {undefined | Number} the room users number if available + */ + var getUsersCountInRoom = function(room_id) { + var memberCount; + + var room = $rootScope.events.rooms[room_id]; + if (room) { + memberCount = 0; + + for (var i in room.members) { + var member = room.members[i]; + + if ("join" === member.membership) { + memberCount = memberCount + 1; + } + } + } + + return memberCount; + }; - var rooms = response.data.rooms; + $scope.onInit = function() { + // Init recents list only once + if ($rootScope.rooms) { + return; + } + + $rootScope.rooms = {}; + + // Use initialSync data to init the recents list + eventHandlerService.waitForInitialSyncCompletion().then( + function(initialSyncData) { + + var rooms = initialSyncData.data.rooms; for (var i=0; i<rooms.length; i++) { var room = rooms[i]; // Add room_alias & room_display_name members - $scope.rooms[room.room_id] = angular.extend(room, matrixService.getRoomAliasAndDisplayName(room)); + $rootScope.rooms[room.room_id] = angular.extend(room, matrixService.getRoomAliasAndDisplayName(room)); // Create a shortcut for the last message of this room if (room.messages && room.messages.chunk && room.messages.chunk[0]) { - $scope.rooms[room.room_id].lastMsg = room.messages.chunk[0]; + $rootScope.rooms[room.room_id].lastMsg = room.messages.chunk[0]; } - } - - var presence = response.data.presence; - for (var i = 0; i < presence.length; ++i) { - eventHandlerService.handleEvent(presence[i], false); + + $rootScope.rooms[room.room_id].numUsersInRoom = getUsersCountInRoom(room.room_id); } // From now, update recents from the stream listenToEventStream(); }, function(error) { - $scope.feedback = "Failure: " + error.data; + $rootScope.feedback = "Failure: " + error.data; } ); }; - $scope.onInit = function() { - eventHandlerService.waitForInitialSyncCompletion().then(function() { - refresh(); - }); - }; - + // Clean data when user logs out + $scope.$on(eventHandlerService.RESET_EVENT, function() { + + delete $rootScope.rooms; + }); }]); diff --git a/webclient/recents/recents.html b/webclient/recents/recents.html index 280d0632ab..3d736b6694 100644 --- a/webclient/recents/recents.html +++ b/webclient/recents/recents.html @@ -9,12 +9,17 @@ {{ room.room_id | mRoomName }} </td> <td class="recentsRoomSummaryTS"> + <span ng-show="undefined !== room.numUsersInRoom"> + {{ room.numUsersInRoom || '1' }} {{ room.numUsersInRoom == 1 ? 'user' : 'users' }} + </span> + </td> + <td class="recentsRoomSummaryTS"> {{ (room.lastMsg.ts) | date:'MMM d HH:mm' }} </td> </tr> <tr> - <td colspan="2" class="recentsRoomSummary"> + <td colspan="3" class="recentsRoomSummary"> <div ng-show="room.membership === 'invite'"> {{ room.lastMsg.inviter | mUserDisplayName: room.room_id }} invited you @@ -61,7 +66,7 @@ </div> <div ng-switch-when="m.emote"> - <span ng-bind-html="'* ' + (room.lastMsg.user_id) + ' ' + room.lastMsg.content.body | linky:'_blank'"> + <span ng-bind-html="'* ' + (room.lastMsg.user_id | mUserDisplayName: room.room_id) + ' ' + room.lastMsg.content.body | linky:'_blank'"> </span> </div> @@ -71,6 +76,14 @@ </div> </div> + <div ng-switch-when="m.room.topic"> + {{ room.lastMsg.user_id | mUserDisplayName: room.room_id }} changed the topic to: {{ room.lastMsg.content.topic }} + </div> + + <div ng-switch-when="m.room.name"> + {{ room.lastMsg.user_id | mUserDisplayName: room.room_id }} changed the room name to: {{ room.lastMsg.content.name }} + </div> + <div ng-switch-default> <div ng-if="room.lastMsg.type.indexOf('m.call.') === 0"> Call diff --git a/webclient/room/room-controller.js b/webclient/room/room-controller.js index c8ca771b25..45dfff95c2 100644 --- a/webclient/room/room-controller.js +++ b/webclient/room/room-controller.js @@ -15,8 +15,8 @@ limitations under the License. */ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput']) -.controller('RoomController', ['$scope', '$timeout', '$routeParams', '$location', '$rootScope', 'matrixService', 'eventHandlerService', 'mFileUpload', 'mPresence', 'matrixPhoneService', 'MatrixCall', - function($scope, $timeout, $routeParams, $location, $rootScope, matrixService, eventHandlerService, mFileUpload, mPresence, matrixPhoneService, MatrixCall) { +.controller('RoomController', ['$filter', '$scope', '$timeout', '$routeParams', '$location', '$rootScope', 'matrixService', 'eventHandlerService', 'mFileUpload', 'mPresence', 'matrixPhoneService', 'MatrixCall', + function($filter, $scope, $timeout, $routeParams, $location, $rootScope, matrixService, eventHandlerService, mFileUpload, mPresence, matrixPhoneService, MatrixCall) { 'use strict'; var MESSAGES_PER_PAGINATION = 30; var THUMBNAIL_SIZE = 320; @@ -27,12 +27,12 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput']) $scope.state = { user_id: matrixService.config().user_id, - events_from: "END", // when to start the event stream from. - earliest_token: "END", // stores how far back we've paginated. + permission_denied: undefined, // If defined, this string contains the reason why the user cannot join the room first_pagination: true, // this is toggled off when the first pagination is done - can_paginate: true, // this is toggled off when we run out of items + can_paginate: false, // this is toggled off when we are not ready yet to paginate or when we run out of items paginating: false, // used to avoid concurrent pagination requests pulling in dup contents - stream_failure: undefined // the response when the stream fails + stream_failure: undefined, // the response when the stream fails + waiting_for_joined_event: false // true when the join request is pending. Back to false once the corresponding m.room.member event is received }; $scope.members = {}; $scope.autoCompleting = false; @@ -42,6 +42,85 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput']) $scope.imageURLToSend = ""; $scope.userIDToInvite = ""; + + // vars and functions for updating the name + $scope.name = { + isEditing: false, + newNameText: "", + editName: function() { + if ($scope.name.isEditing) { + console.log("Warning: Already editing name."); + return; + }; + + // Use the filter applied in html to set the input value + $scope.name.newNameText = $filter('mRoomName')($scope.room_id); + + // Force focus to the input + $timeout(function() { + angular.element('.roomNameInput').focus(); + }, 0); + + $scope.name.isEditing = true; + }, + updateName: function() { + console.log("Updating name to "+$scope.name.newNameText); + matrixService.setName($scope.room_id, $scope.name.newNameText).then( + function() { + }, + function(error) { + $scope.feedback = "Request failed: " + error.data.error; + } + ); + + $scope.name.isEditing = false; + }, + cancelEdit: function() { + $scope.name.isEditing = false; + } + }; + + // vars and functions for updating the topic + $scope.topic = { + isEditing: false, + newTopicText: "", + editTopic: function() { + if ($scope.topic.isEditing) { + console.log("Warning: Already editing topic."); + return; + } + var topicEvent = $rootScope.events.rooms[$scope.room_id]['m.room.topic']; + if (topicEvent) { + $scope.topic.newTopicText = topicEvent.content.topic; + } + else { + $scope.topic.newTopicText = ""; + } + + // Force focus to the input + $timeout(function() { + angular.element('.roomTopicInput').focus(); + }, 0); + + $scope.topic.isEditing = true; + }, + updateTopic: function() { + console.log("Updating topic to "+$scope.topic.newTopicText); + matrixService.setTopic($scope.room_id, $scope.topic.newTopicText).then( + function() { + }, + function(error) { + $scope.feedback = "Request failed: " + error.data.error; + } + ); + + $scope.topic.isEditing = false; + }, + cancelEdit: function() { + $scope.topic.isEditing = false; + } + }; + var scrollToBottom = function(force) { console.log("Scrolling to bottom"); @@ -81,8 +160,37 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput']) $scope.$on(eventHandlerService.MEMBER_EVENT, function(ngEvent, event, isLive) { if (isLive) { - scrollToBottom(); - updateMemberList(event); + if ($scope.state.waiting_for_joined_event) { + // The user has successfully joined the room, we can getting data for this room + $scope.state.waiting_for_joined_event = false; + onInit3(); + } + else if (event.state_key === $scope.state.user_id && "invite" !== event.membership && "join" !== event.membership) { + var user; + + if ($scope.members[event.user_id]) { + user = $scope.members[event.user_id].displayname; + } + if (user) { + user = user + " (" + event.user_id + ")"; + } + else { + user = event.user_id; + } + + + if ("ban" === event.membership) { + $scope.state.permission_denied = "You have been banned by " + user; + } + else { + $scope.state.permission_denied = "You have been kicked by " + user; + } + + } + else { + scrollToBottom(); + updateMemberList(event); + } } }); @@ -119,12 +227,15 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput']) else { $scope.state.paginating = true; } - // console.log("paginateBackMessages from " + $scope.state.earliest_token + " for " + numItems); + + console.log("paginateBackMessages from " + $rootScope.events.rooms[$scope.room_id].pagination.earliest_token + " for " + numItems); var originalTopRow = $("#messageTable>tbody>tr:first")[0]; - matrixService.paginateBackMessages($scope.room_id, $scope.state.earliest_token, numItems).then( + + // Paginate events from the point in cache + matrixService.paginateBackMessages($scope.room_id, $rootScope.events.rooms[$scope.room_id].pagination.earliest_token, numItems).then( function(response) { - eventHandlerService.handleEvents(response.data.chunk, false); - $scope.state.earliest_token = response.data.end; + + eventHandlerService.handleRoomMessages($scope.room_id, response.data, false); if (response.data.chunk.length < MESSAGES_PER_PAGINATION) { // no more messages to paginate. this currently never gets turned true again, as we never // expire paginated contents in the current implementation. @@ -419,7 +530,7 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput']) var powerLevel = 50; // default power level for op if (matches) { var user_id = matches[1]; - if (matches.length === 4) { + if (matches.length === 4 && undefined !== matches[3]) { powerLevel = parseInt(matches[3]); } if (powerLevel !== NaN) { @@ -472,8 +583,7 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput']) room_id: $scope.room_id, type: "m.room.message", user_id: $scope.state.user_id, - // FIXME: re-enable echo_msg_state when we have a nice way to turn the field off again - // echo_msg_state: "messagePending" // Add custom field to indicate the state of this fake message to HTML + echo_msg_state: "messagePending" // Add custom field to indicate the state of this fake message to HTML }; $scope.textInput = ""; @@ -482,26 +592,22 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput']) } if (promise) { + // Reset previous feedback + $scope.feedback = ""; + promise.then( - function() { + function(response) { console.log("Request successfully sent"); - if (!echo) { - $scope.textInput = ""; - } -/* - if (echoMessage) { - // Remove the fake echo message from the room messages - // It will be replaced by the one acknowledged by the server - // ...except this causes a nasty flicker. So don't swap messages for now. --matthew - // var index = $rootScope.events.rooms[$scope.room_id].messages.indexOf(echoMessage); - // if (index > -1) { - // $rootScope.events.rooms[$scope.room_id].messages.splice(index, 1); - // } + + if (echo) { + // Mark this fake message event with its allocated event_id + // When the true message event will come from the events stream (in handleMessage), + // we will be able to replace the fake one by the true one + echoMessage.event_id = response.data.event_id; } else { $scope.textInput = ""; - } -*/ + } }, function(error) { $scope.feedback = "Request failed: " + error.data.error; @@ -596,14 +702,18 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput']) // Do we to join the room before starting? if (needsToJoin) { + $scope.state.waiting_for_joined_event = true; matrixService.join($scope.room_id).then( function() { + // onInit3 will be called once the joined m.room.member event is received from the events stream + // This avoids to get the joined information twice in parallel: + // - one from the events stream + // - one from the pagination because the pagination window covers this event ts console.log("Joined room "+$scope.room_id); - onInit3(); }, function(reason) { console.log("Can't join room: " + JSON.stringify(reason)); - $scope.feedback = "You do not have permission to join this room"; + $scope.state.permission_denied = "You do not have permission to join this room"; }); } else { @@ -615,9 +725,6 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput']) var onInit3 = function() { console.log("onInit3"); - - // TODO: We should be able to keep them - eventHandlerService.resetRoomMessages($scope.room_id); // Make recents highlight the current room $scope.recentsSelectedRoomID = $scope.room_id; @@ -635,21 +742,24 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput']) // Arm list timing update timer updateMemberListPresenceAge(); + + // Start pagination + $scope.state.can_paginate = true; + paginate(MESSAGES_PER_PAGINATION); }, function(error) { $scope.feedback = "Failed get member list: " + error.data.error; } ); - - paginate(MESSAGES_PER_PAGINATION); }; - $scope.inviteUser = function(user_id) { + $scope.inviteUser = function() { - matrixService.invite($scope.room_id, user_id).then( + matrixService.invite($scope.room_id, $scope.userIDToInvite).then( function() { console.log("Invited."); - $scope.feedback = "Invite sent successfully"; + $scope.feedback = "Invite successfully sent to " + $scope.userIDToInvite; + $scope.userIDToInvite = ""; }, function(reason) { $scope.feedback = "Failure: " + reason; @@ -710,7 +820,15 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput']) var call = new MatrixCall($scope.room_id); call.onError = $rootScope.onCallError; call.onHangup = $rootScope.onCallHangup; - call.placeCall(); + call.placeCall({audio: true, video: false}); + $rootScope.currentCall = call; + }; + + $scope.startVideoCall = function() { + var call = new MatrixCall($scope.room_id); + call.onError = $rootScope.onCallError; + call.onHangup = $rootScope.onCallHangup; + call.placeCall({audio: true, video: true}); $rootScope.currentCall = call; }; diff --git a/webclient/room/room.html b/webclient/room/room.html index 5bd2cc92d5..9d617eadd8 100644 --- a/webclient/room/room.html +++ b/webclient/room/room.html @@ -2,8 +2,31 @@ <div id="roomHeader"> <a href ng-click="goToPage('/')"><img src="img/logo-small.png" width="100" height="43" alt="[matrix]"/></a> - <div id="roomName"> - {{ room_id | mRoomName }} + <div class="roomHeaderInfo"> + + <div class="roomNameSection"> + <div ng-hide="name.isEditing" ng-dblclick="name.editName()" id="roomName"> + {{ room_id | mRoomName }} + </div> + <form ng-submit="name.updateName()" ng-show="name.isEditing" class="roomNameForm"> + <input ng-model="name.newNameText" ng-blur="name.cancelEdit()" class="roomNameInput" /> + </form> + </div> + + <div class="roomTopicSection"> + <button ng-hide="events.rooms[room_id]['m.room.topic'].content.topic || topic.isEditing" + ng-click="topic.editTopic()" class="roomTopicSetNew"> + Set Topic + </button> + <div ng-show="events.rooms[room_id]['m.room.topic'].content.topic || topic.isEditing"> + <div ng-hide="topic.isEditing" ng-dblclick="topic.editTopic()" id="roomTopic"> + {{ events.rooms[room_id]['m.room.topic'].content.topic | limitTo: 200}} + </div> + <form ng-submit="topic.updateTopic()" ng-show="topic.isEditing" class="roomTopicForm"> + <input ng-model="topic.newTopicText" ng-blur="topic.cancelEdit()" class="roomTopicInput" /> + </form> + </div> + </div> </div> </div> @@ -14,14 +37,14 @@ <div ng-include="'recents/recents.html'"></div> </div> - <div id="usersTableWrapper"> + <div id="usersTableWrapper" ng-hide="state.permission_denied"> <table id="usersTable"> <tr ng-repeat="member in members | orderMembersList"> <td class="userAvatar mouse-pointer" ng-click="$parent.goToUserPage(member.id)" ng-class="member.membership == 'invite' ? 'invited' : ''"> <img class="userAvatarImage" ng-src="{{member.avatar_url || 'img/default-profile.png'}}" alt="{{ member.displayname || member.id.substr(0, member.id.indexOf(':')) }}" - title="{{ member.id }}" + title="{{ member.id }} - power: {{ member.powerLevel }}" width="80" height="80"/> <img class="userAvatarGradient" src="img/gradient.png" title="{{ member.id }}" width="80" height="24"/> <div class="userPowerLevel" ng-style="{'width': member.powerLevelNorm +'%'}"></div> @@ -33,7 +56,7 @@ </table> </div> - <div id="messageTableWrapper" keep-scroll> + <div id="messageTableWrapper" ng-hide="state.permission_denied" keep-scroll> <!-- FIXME: need to have better timestamp semantics than the (msg.content.hsob_ts || msg.ts) hack below --> <table id="messageTable" infinite-scroll="paginateMore()"> <tr ng-repeat="msg in events.rooms[room_id].messages" @@ -49,7 +72,7 @@ <img class="avatarImage" ng-src="{{ members[msg.user_id].avatar_url || 'img/default-profile.png' }}" width="32" height="32" ng-hide="events.rooms[room_id].messages[$index - 1].user_id === msg.user_id || msg.user_id === state.user_id"/> </td> - <td ng-class="!msg.content.membership ? (msg.content.msgtype === 'm.emote' ? 'emote text' : 'text') : 'membership text'"> + <td ng-class="(!msg.content.membership && ('m.room.topic' !== msg.type && 'm.room.name' !== msg.type))? (msg.content.msgtype === 'm.emote' ? 'emote text' : 'text') : 'membership text'"> <div class="bubble"> <span ng-if="'join' === msg.content.membership"> {{ members[msg.state_key].displayname || msg.state_key }} joined @@ -76,10 +99,18 @@ </span> </span> - <span ng-show='msg.content.msgtype === "m.emote"' ng-bind-html="'* ' + (members[msg.user_id].displayname || msg.user_id) + ' ' + msg.content.body | linky:'_blank'"/> + <span ng-show='msg.content.msgtype === "m.emote"' + ng-class="msg.echo_msg_state" + ng-bind-html="'* ' + (members[msg.user_id].displayname || msg.user_id) + ' ' + msg.content.body | linky:'_blank'"/> + <span ng-show='msg.content.msgtype === "m.text"' + class="message" ng-class="msg.echo_msg_state" ng-bind-html="((msg.content.msgtype === 'm.text') ? msg.content.body : '') | linky:'_blank'"/> + + <span ng-show='msg.type === "m.call.invite" && msg.user_id == state.user_id'>Outgoing Call</span> + <span ng-show='msg.type === "m.call.invite" && msg.user_id != state.user_id'>Incoming Call</span> + <div ng-show='msg.content.msgtype === "m.image"'> <div ng-hide='msg.content.thumbnail_url' ng-style="msg.content.body.h && { 'height' : (msg.content.body.h < 320) ? msg.content.body.h : 320}"> <img class="image" ng-src="{{ msg.content.url }}"/> @@ -89,6 +120,15 @@ ng-click="$parent.fullScreenImageURL = msg.content.url"/> </div> </div> + + <span ng-if="'m.room.topic' === msg.type"> + {{ members[msg.user_id].displayname || msg.user_id }} changed the topic to: {{ msg.content.topic }} + </span> + + <span ng-if="'m.room.name' === msg.type"> + {{ members[msg.user_id].displayname || msg.user_id }} changed the room name to: {{ msg.content.name }} + </span> + </div> </td> <td class="rightBlock"> @@ -98,6 +138,10 @@ </tr> </table> </div> + + <div ng-show="state.permission_denied"> + {{ state.permission_denied }} + </div> </div> </div> @@ -110,11 +154,13 @@ {{ state.user_id }} </td> <td width="*"> - <input id="mainInput" ng-model="textInput" ng-enter="send()" ng-focus="true" autocomplete="off" tab-complete/> + <textarea id="mainInput" rows="1" ng-model="textInput" ng-enter="send()" + ng-disabled="state.permission_denied" + ng-focus="true" autocomplete="off" tab-complete/> </td> <td id="buttonsCell"> - <button ng-click="send()">Send</button> - <button m-file-input="imageFileToSend" class="extraControls">Image</button> + <button ng-click="send()" ng-disabled="state.permission_denied">Send</button> + <button m-file-input="imageFileToSend" class="extraControls" ng-disabled="state.permission_denied">Image</button> </td> </tr> </table> @@ -122,11 +168,11 @@ <div class="extraControls"> <span> Invite a user: - <input ng-model="userIDToInvite" size="32" type="text" placeholder="User ID (ex:@user:homeserver)"/> - <button ng-click="inviteUser(userIDToInvite)">Invite</button> + <input ng-model="userIDToInvite" size="32" type="text" ng-enter="inviteUser()" ng-disabled="state.permission_denied" placeholder="User ID (ex:@user:homeserver)"/> + <button ng-click="inviteUser()" ng-disabled="state.permission_denied">Invite</button> </span> - <button ng-click="leaveRoom()">Leave</button> - <button ng-click="startVoiceCall()" ng-show="(currentCall == undefined || currentCall.state == 'ended') && memberCount() == 2">Voice Call</button> + <button ng-click="leaveRoom()" ng-disabled="state.permission_denied">Leave</button> + <button ng-click="startVoiceCall()" ng-show="(currentCall == undefined || currentCall.state == 'ended') && memberCount() == 2" ng-disabled="state.permission_denied">Voice Call</button> </div> {{ feedback }} diff --git a/webclient/settings/settings.html b/webclient/settings/settings.html index 924812e7ae..c358a6e9d8 100644 --- a/webclient/settings/settings.html +++ b/webclient/settings/settings.html @@ -81,6 +81,7 @@ <ul> <li>/nick <display_name>: change your display name</li> <li>/me <action>: send the action you are doing. /me will be replaced by your display name</li> + <li>/join <room_alias>: join a room</li> <li>/kick <user_id> [<reason>]: kick the user</li> <li>/ban <user_id> [<reason>]: ban the user</li> <li>/unban <user_id>: unban the user</li> |