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>
|